Bir proje büyüdükçe state yönetimi kütüphanesi seçimi, ekip toplantısında "hangisini biliyoruz" sorusuyla karara bağlanır; bundle'a ne kadar ağırlık ekleyeceği çoğu zaman sonradan, bundle analiz raporunda fark edilir. Redux Toolkit, Zustand, Jotai ve Recoil aynı problemi çözer: bileşenler arasında paylaşılan durumu tutmak ve değiştiğinde ilgili yerleri güncellemek. Ama dördü de bu işi farklı bir mimariyle yapar ve bu mimari farkı, gzip sonrası kilobayt sayısına da, çalışma zamanında kaç bileşenin yeniden render olacağına da doğrudan yansır.

Sorun, kütüphaneyi "popüler olduğu için" ya da "eskiden kullandık" diye seçmekten çok, hangi maliyetin projenin büyüklüğüyle orantılı olduğunu değerlendirmemekten doğuyor. Küçük bir formun state'i için Redux kurmak, büyük bir dashboard'un state'i için Zustand'ın düz store'una güvenmek; her ikisi de teknik olarak çalışır, ama biri gereksiz ağırlık taşır, diğeri büyüdükçe bakım sorunu üretir.

Aşağıda dört kütüphanenin bundle boyutu, mimari farkı ve bu mimarinin runtime'da ürettiği re-render maliyeti sırayla ele alınıyor.

Dört kütüphane aynı işi görür, dört farklı bundle maliyetiyle

Bundle boyutu karşılaştırması yapılırken önce neyin ölçüldüğü netleşmeli. Kütüphanenin kendi minified+gzip boyutu bir şeydir, projeye kurulumu için gereken bağımlı paketlerin toplamı başka bir şeydir. Redux'un kendisi küçüktür, ama pratikte kimse yalnızca redux paketini kurmaz; @reduxjs/toolkit ve react-redux birlikte gelir, ve Toolkit varsayılan olarak immer'i de içine alır.

Kabaca sıralama şöyle oturur: Zustand gzip sonrası yaklaşık 1 kB civarında, hiçbir zorunlu bağımlılığı yok. Jotai çekirdek paketi yaklaşık 2-3 kB aralığında, o da bağımsız. Redux Toolkit + react-redux birlikte yaklaşık 12-13 kB civarına çıkıyor; immer'in getirdiği proxy tabanlı immutable güncelleme mantığı bu rakamın büyük kısmını oluşturuyor. Recoil ise dördü arasında en ağırı; kendi zamanlama mekanizmasını ve dahili snapshot altyapısını taşıdığı için gzip sonrası 20 kB'ın üzerine çıkabiliyor.

Bu farklar tek bir sayfa uygulamasında gözle görülür bir etki yaratmayabilir. Ama bundle boyutu kontrolü yapılan her projede olduğu gibi, bu paketler diğer bağımlılıklarla toplanınca fark birikir; özellikle mobil ağ koşullarında ilk indirme süresine yansır.

Redux Toolkit: ekosistem gücü, boilerplate yerine bundle ağırlığı

Redux Toolkit'in ağırlığı tesadüf değil; middleware zinciri, DevTools entegrasyonu, createSlice'ın arkasındaki immer entegrasyonu ve RTK Query gibi opsiyonel modüller hep aynı paketin parçası olarak gelir. Bu, büyük ekiplerde standart bir yapı sağlar: her slice aynı desenle yazılır, her middleware aynı noktadan eklenir, zaman içinde geçmişe gidip state değişikliklerini incelemek mümkün olur.

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; }, // immer sayesinde mutasyon gibi yazılır
  },
});

Bu kolaylığın bedeli, immer'in her state güncellemesinde proxy tabanlı bir izleme katmanı çalıştırmasıdır. Küçük state ağaçlarında bu maliyet ölçülemeyecek kadar düşüktür, ama binlerce alanlı, derin iç içe geçmiş bir state ağacında her güncelleme immer'in proxy oluşturma ve draft birleştirme işini tetikler. Bu, memoization'ın kendisinin de ücretsiz olmadığı mantığıyla aynı kategoriye giriyor: soyutlama katmanı, altında çözdüğü problemden daha pahalıya gelebiliyor.

Redux Toolkit paketinin isteğe bağlı ama sık tercih edilen bir parçası da RTK Query. Veri çekme, önbellekleme ve invalidation mantığını tek bir API altında topluyor, ayrı bir data-fetching kütüphanesi kurma ihtiyacını ortadan kaldırıyor. Ama bu rahatlık paket boyutuna ekleniyor; RTK Query import edildiğinde toplam paket birkaç kilobayt daha büyüyor ve bu modül fiilen kullanılmasa bile Toolkit'in ana paketinden her zaman temiz şekilde ayrılamıyor. Projede zaten ayrı bir veri çekme katmanı varsa, RTK Query'yi hiç import etmemek bu ek yükü tamamen ortadan kaldırıyor.

Zustand: minimal API'nin bundle karşılığı

Zustand'ın küçüklüğü, mimari bir tercihin doğrudan sonucu. Kütüphane, React'in kendi useSyncExternalStore hook'unun üzerine ince bir katman kurar; middleware, devtools entegrasyonu, immer desteği gibi her şey opsiyonel eklenti olarak dışarıda tutulur, varsayılan kurulumda projeye girmez.

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// Bileşen yalnızca ihtiyacı olan dilimi seçer
const count = useStore((state) => state.count);

Bu tasarımın avantajı, kullanılmayan özelliğin bundle'a hiç girmemesi; tree shaking'in bu kadar temiz çalıştığı az sayıda state kütüphanesinden biridir. Dezavantajı ise ekosistemin daha küçük olması: Redux'taki gibi hazır middleware kütüphanesi bolluğu yok, büyük ekiplerde standart bir yapı kurmak için kendi konvansiyonunuzu yazmanız gerekiyor. Küçük ve orta ölçekli projelerde bu bir kısıtlama değil, ama yüzlerce slice'a bölünecek büyüklükte bir uygulamada disiplin eksikliği çabuk fark ediliyor.

Persist eklentisi gibi orta katman ihtiyaçları doğduğunda da Zustand aynı prensibi koruyor: persist middleware'i yalnızca kullanan projede bundle'a giriyor, kullanmayan projede hiç yer kaplamıyor. Bu, Redux ekosisteminde redux-persist gibi ayrı bir paket kurmaktan farklı değil aslında, ama Zustand'da bu ek katman çekirdek API'nin üzerine ince bir sarmalayıcı olarak eklendiği için toplam ağırlık daha öngörülebilir kalıyor.

Jotai ve Recoil: atomic model runtime maliyeti nasıl değiştirir

Jotai ve Recoil, Redux ve Zustand'dan farklı bir modele dayanıyor: tek bir merkezi store yerine, her biri bağımsız güncellenebilen küçük "atom" birimleri tanımlıyorsunuz. Bu model, bundle boyutundan bağımsız olarak runtime davranışını da değiştiriyor.

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Jotai'nin çekirdek paketi küçük kalmayı başarıyor çünkü atom grafiği React'in kendi render döngüsüne yakın çalışıyor, ayrı bir zamanlama katmanı gerektirmiyor. Recoil ise aynı atomic fikri, kendi bağımsız zamanlayıcısı, seçici (selector) önbellekleme katmanı ve geliştirici araçlarıyla birlikte sunuyor; bu da hem bundle boyutunu hem de her atom güncellemesinin arkasında çalışan iş miktarını artırıyor. İki kütüphane aynı zihinsel modeli paylaşsa da, biri minimal bir çekirdek üzerine kurulu, diğeri kendi çalışma zamanını taşıyor.

Selector granülerliği: bundle boyutundan daha büyük fark yaratan re-render maliyeti

Bundle boyutu tek başına projeyi yavaşlatmıyor; asıl fark, state güncellendiğinde kaç bileşenin yeniden render olduğunda ortaya çıkıyor, ve bu konuda dört kütüphane arasındaki ayrım bundle farkından çok daha büyük.

Redux'ta useSelector ile doğru dilimi seçmezseniz, ilgisiz bir state değişikliği bile bileşeni yeniden render eder; bu, render maliyetinin yapısal olarak nasıl düştüğü konusuyla doğrudan ilişkili bir hatadır. Zustand'da aynı risk var: store'un tamamını değil, yalnızca ihtiyaç duyulan alanı seçen bir selector yazılmazsa, store'daki herhangi bir değişiklik bileşeni tetikler.

// Riskli: tüm store'u okuyor, herhangi bir alan değiştiğinde render tetiklenir
const state = useStore((s) => s);

// Doğru: yalnızca ihtiyaç duyulan alanı okuyor
const count = useStore((s) => s.count);

Jotai ve Recoil'de bu granülerlik varsayılan olarak geliyor; her bileşen yalnızca abone olduğu atomun değişiminden haberdar oluyor, başka bir atomun güncellenmesi o bileşeni etkilemiyor. Bu, büyük state ağaçlarında Redux ya da dikkatsiz kurulmuş bir Zustand store'undan daha az gereksiz render üretebiliyor demek, ama bu avantaj yalnızca atom sayısı makul tutulduğunda geçerli; yüzlerce ince taneli atom, bu sefer de her birinin ayrı abonelik yönetimi maliyetini biriktiriyor.

Kütüphaneyi değiştirmenin maliyeti seçmekten daha yüksektir

Bu dört kütüphane arasında bir geçiş yapmak, ilk seçimi yapmaktan çok daha pahalı bir iştir; bu yüzden karar aşamasında bundle ve render maliyetini birlikte değerlendirmek, sonradan düzeltmekten her zaman ucuza gelir. Redux'tan Zustand'a geçiş, teorik olarak selector mantığının yeniden yazılmasından ibaret gibi görünür, ama pratikte middleware zincirine bağlı yan etkiler (logging, analytics tetikleyicileri, persist katmanı) birer birer yeniden kurulmalıdır. Jotai'den Recoil'e ya da tersine geçişte ise atom tanımlarının kendisi değişmese de, seçici (selector) API'lerinin farklı isimlendirmesi ve farklı önbellekleme davranışı, görünürde küçük ama testte fark edilen regresyonlara yol açabilir.

Bu nedenle büyüyen bir projede erken aşamada "hangi kütüphaneyi seçtik" sorusundan daha önemlisi "state ağacımız ne kadar büyüyecek, kaç bileşen aynı veriyi okuyacak" sorusunu cevaplamaktır. Küçük bir MVP'nin state ihtiyacı ile altı ay sonraki halinin ihtiyacı aynı değildir; kütüphane seçimini bu büyüme eğrisine göre yapmak, sonradan zorunlu bir geçiş yerine planlı bir ölçeklenme sağlar.

Proje büyüklüğüne göre karar çerçevesi

Bundle ve runtime maliyetlerini yan yana koyduğunuzda karar, proje büyüklüğü ve ekip yapısına göre netleşiyor:

  • Az sayıda global state parçası olan küçük-orta projeler için Zustand, bundle maliyeti en düşük seçenek; ek middleware ihtiyacı doğduğunda opsiyonel olarak eklenebiliyor.
  • Bağımsız, birbirinden izole state parçalarının çok olduğu, form yoğun ya da granüler abonelik gerektiren arayüzlerde Jotai, küçük bundle ile ince taneli güncellemeyi bir arada veriyor.
  • Büyük ekiplerin ortak bir konvansiyona ihtiyaç duyduğu, zaman içinde state değişikliklerini incelemenin (time-travel debugging) önemli olduğu kurumsal projelerde Redux Toolkit'in bundle maliyeti, standartlaşma ve araç desteğiyle telafi ediliyor.
  • Recoil, deneysel API yüzeyi ve göreceli olarak yüksek bundle maliyeti nedeniyle yeni projelerde nadiren ilk tercih; mevcut Recoil tabanlı bir kod tabanını sürdürmek başka, sıfırdan başlarken seçmek başka bir karar.

Bu liste bir kütüphaneyi diğerine mutlak üstün kılmıyor; hangi maliyetin projenizin büyüklüğüyle orantılı olduğunu soruyor. Küçük bir landing page'de Redux Toolkit kurmak, dinamik olarak bölünebilecek bir özelliği baştan tüm sayfaya yaymak kadar gereksiz bir ön yatırım.

Seçimi doğrulamak: ölçüm adımları

Kütüphane seçimi bir kez yapılıp unutulacak bir karar değil; projenin state karmaşıklığı arttıkça yeniden değerlendirilmesi gereken bir tercih. Doğrulama için üç adım yeterli: önce bundle analiz aracıyla seçilen kütüphanenin projeye kaç kilobayt eklediğini ölçün, ikinci olarak React DevTools Profiler ile bir state güncellemesinin kaç bileşeni yeniden render ettiğini kaydedin, üçüncü olarak store yapısını değiştirdikten sonra aynı ölçümü tekrarlayıp farkı doğrulayın.

Bundle analizi raporunda state kütüphanesinin payı beklenenden büyükse, bu genelde tek bir paketin değil, birlikte kurulan yardımcı paketlerin (middleware, devtools, persist eklentisi) toplamının sonucu oluyor; her birini ayrı ayrı değerlendirmek gerekiyor. Profiler tarafında ise asıl soru şu: state güncellendiğinde yalnızca ilgili bileşen mi render oluyor, yoksa ilgisiz bir alt ağaç da tetikleniyor mu? Cevap ikincisiyse, sorun kütüphanenin kendisinde değil, selector'ların nasıl yazıldığında.

Bu ölçüm alışkanlığı olmadan yapılan seçim, birkaç ay sonra "neden bu kadar yavaş" sorusuyla geri dönüyor; o noktada kütüphaneyi değiştirmek, baştan doğru ölçüp karar vermekten çok daha pahalı bir iş haline geliyor.