useEffect bağımlılık dizisi yanlış yazıldığında bileşen çalışmaya devam eder, ama altta sürekli gereksiz iş üretilir. Eksik bağımlılık stale closure yaratır, fazla veya dengesiz bağımlılık sonsuz döngüye iter. React Profiler ve Chrome DevTools Performance paneli bu iki farklı sorunu hem React katmanında hem tarayıcı katmanında tespit etmeye yarar.
useEffect Dependency Array Yanlış Kullanımı Performansı Nasıl Öldürür?
React uygulamalarında performans düşüşünün kaynağı çoğunlukla ağ gecikmesi veya büyük bundle boyutu değildir. useEffect bağımlılık dizisi yanlış yazıldığında bileşen çalışmaya devam eder, sayfa görünürde işler, ama altta sürekli gereksiz iş üretilir. Bu tür sorunlar çoğu zaman geliştirme ortamında fark edilmez; kullanıcıya dönük bir hata olmadığı için gizlenip kalır.
Bağımlılık dizisi, React'e bir effect'in ne zaman yeniden çalışması gerektiğini söyleyen bir sözleşmedir. Boş dizi verilirse effect yalnızca mount sırasında çalışır; değerler girilirse yalnızca o değerler değiştiğinde; hiç verilmezse her render'da. Her seçenek doğru bağlamda geçerlidir, yanlış bağlamda ise farklı biçimlerde sorun üretir. Sorun genellikle iki kümede toplanır: eksik bağımlılık ve fazla ya da dengesiz bağımlılık.
Eksik bağımlılık, effect'i eski bir değere kilitler ve stale closure (eskimiş kapatma) yaratır. Fazla bağımlılık veya referans dengesizliği ise effect'i gereğinden sık tetikler, bazı durumlarda döngüye sokar. React Profiler ve Chrome DevTools, bu iki senaryoyu render sayısı ve long task izinden ayırır.
Referans eşitliği: React bağımlılığı nasıl karşılaştırır?
React, bağımlılık dizisindeki değerleri Object.is ile karşılaştırır. Sayılar ve string'ler için bu davranış beklentiye uygundur: aynı sayı değerleri eşit çıkar. Nesneler ve diziler için durum farklıdır. İçerikleri aynı olsa bile bellekte farklı referanslara sahip iki nesne, Object.is karşılaştırmasında eşit değildir.
Bir bileşen her render döngüsünde const config = { timeout: 3000 } gibi bir nesne tanımlıyorsa ve bu nesne bağımlılık dizisine ekleniyorsa, effect her render'da tetiklenir. Çünkü her render yeni bir referans üretir; React bunu "değer değişti" olarak yorumlar. Değişen hiçbir şey yoktur, ama React'in bakış açısından her şey değişmiştir.
Fonksiyonlar da aynı şekilde davranır. Ebeveyn bileşenden bir callback alınıyorsa ve bu callback useCallback ile sarılmamışsa, her render'da yeni bir fonksiyon referansı gelir. Effect bu callback'i bağımlılık olarak izliyorsa, her ebeveyn render'ında child bileşenin effect'i de yeniden çalışır; ebeveynin nedeni ne olursa olsun, style değişimi, başka bir state güncellemesi, herhangi bir şey.
Primitive değerleri bağımlılığa eklemek, nesnelerin kendisini eklemekten daha güvenlidir. options yerine options.timeout, user yerine user.id kullanmak, referans dengesizliğinden kaynaklanan gereksiz tetiklemeleri doğrudan engeller. Ayrıca bağımlılık listesinin amacını netleştirir: hangi verinin değişimi bu effect'i tetiklemeli sorusunun cevabı, nesnenin tamamı değil içindeki anlamlı alan olduğunda diziye yansıması da o olmalıdır.
Dizi türü bağımlılıklar da aynı tuzağı taşır. İçeriği değişmemiş bir dizi her render'da yeniden oluşturuluyorsa, effect onu her seferinde farklı bir değer olarak algılar. Bu davranış özellikle API yanıtlarını doğrudan bağımlılığa ekleyen yapılarda sık görülür: her fetch tamamlandığında yeni bir dizi referansı gelir, effect yeniden çalışır, yeni bir fetch başlar.
Sonsuz döngü: oluşma mekanizması ve belirtileri
Döngü genellikle şu zincirle oluşur: effect çalışır, state günceller, render tetikler, bağımlılıktaki nesne yeni referans alır, effect yeniden çalışır. Tarayıcı bu döngüde takılır; CPU kullanımı anlık olarak yükselir, sekme yanıt vermez ya da tarayıcı "sayfa yanıt vermiyor" uyarısı gösterir.
Döngüyü tetikleyen tipik bir örünek şöyle görünür: effect içinde bir fetch çağrısı, dönen veri state'e yazılır, state bağımlılık dizisinde bir nesne olarak yer alır. Verinin referansı her fetch döngüsünde değişir. Effect yeniden çalışır, fetch yeniden gönderilir. Ağ sekmesinde art arda dizilen aynı istek, döngünün en açık işaretidir.
Tarayıcı konsolu da ipucu verir. React 18 Strict Mode'da effect'ler geliştirme ortamında kasıtlı olarak iki kez çalıştırılır; bu özellik sayesinde cleanup eksikliği ya da döngüye yatkın yapılar erken fark edilebilir. İki çalıştırma normaldir, yüzlerce çalıştırma değildir. Console'da art arda baskılanan log satırları bir döngünün sinyalidir.
setState çağrısı içeren her effect potansiyel bir döngü adayıdır. Effect bir state'i güncelliyorsa ve o state ya da ondan türetilmiş bir nesne bağımlılık dizisindeyse, döngü kapıdadır. Kodu okumak bu yapıyı genellikle gösterir; her zaman runtime hatası beklemeye gerek yoktur. Veri akışını gözle izlemek, "bu değer değişirse effect çalışır, effect çalışırsa bu değer değişir mi?" sorusunu sormak, döngüyü yazmadan önce fark ettirir.
Eksik bağımlılık ve stale closure
Bağımlılık dizisi kasıtlı olarak eksik bırakılır. Effect içinde kullanılan bir değer diziye eklenmediğinde React uyarmaz; eslint-plugin-react-hooks uyarır. Bu uyarı yok sayıldığında effect, başladığı andaki değeri dondurur ve sonraki render'larda bu eski değerle çalışmaya devam eder. Stale closure problemi budur.
Sessiz kalması stale closure'ı tehlikeli yapar. Effect görünürde çalışır, bileşen render edilir, ancak döndürülen veri veya gerçekleştirilen işlem güncel state'i değil eski state'i yansıtır. Kullanıcı arayüzden bir değer değiştirdiğinde effect onu görmez; race condition olmadığı için hata da fırlatmaz. Yanlış sonuç üretir ve sessiz kalır.
Zamanlayıcılar bu sorunu en belirgin biçimde ortaya çıkarır. setInterval içinde state değerine erişiliyorsa ve interval bir kez kurulup boş bağımlılık dizisiyle mount'a bağlanmışsa, interval her zaman mount anındaki değeri okur. State 0'dan başlayıp kullanıcı etkileşimiyle 10'a ulaşsa bile interval 0'la çalışmaya devam eder. Bu hatayı üretmek kolaydır, fark etmek ise state değeri UI'ya yansımıyorsa zordur.
Düzeltme için iki seçenek vardır. Birincisi, eksik değeri bağımlılığa eklemek ve effect'in her değişimde yeniden kurulmasına izin vermek. İkincisi, değer state ise fonksiyonel setState güncelleyicisi kullanmak: setState(prev => prev + 1) biçimi, güncel state'i bağımlılık olmadan doğrudan alır. Hangisinin uygun olduğu kullanım senaryosuna göre değişir; her zaman tek bir doğru cevap yoktur, ama her iki seçenek de linter uyarısını susturmak için boş dizi bırakmaktan iyidir.
React Profiler ile gereksiz render tespiti
React DevTools'un Profiler sekmesi, hangi bileşenin ne sıklıkla render edildiğini ve her render'ın ne kadar sürdüğünü görselleştirir. Bir effect döngüsü ya da gereksiz tetiklenme varsa Profiler'da belirgin bir kalıp çıkar: aynı bileşen art arda, milisaniyeler içinde defalarca render edilir.
Kayıt başlatılır, sorunlu etkileşim tetiklenir, kayıt durdurulur. Flame graph ve ranked chart görünümleri, hangi bileşenin en çok render edildiğini gösterir. "Why did this render?" bölümü prop veya state değişimini raporlar; referans dengesizliğini doğrudan adlandırmaz, ama render sıklığı anomalisi oraya yönlendirir. Hangi prop'un değiştiği görünüyorsa ve o prop bir nesne veya fonksiyonsa, referans sorunu akla gelmelidir.
Profiler'ın en kullanışlı yanı, render süresini bileşen bazında ortaya koymasıdır. Bir bileşen kısa sürede 50 kez render ediliyorsa bu sayı normalin dışındadır; neden bu kadar render edildiği sorusu buradan başlar. Effect tetiklenme sayısını doğrudan göstermez, ama render sayısı üzerinden dolaylı olarak işaret eder: effect yalnızca bağımlılık değişiminde çalışıyorsa, render sayısı effect tetiklenme üst sınırını da verir.
Profiler ile tanınan başka bir kalıp: üst bileşenin her render'ında alt bileşen de render ediliyorsa ve alt bileşende bir effect tetikleniyorsa, kök neden üsttedir. Üst bileşen neden render ediliyor sorusu çözülmeden alt bileşendeki bağımlılık sorununa müdahale etmek yüzeysel kalır. Ağaçta yukarı doğru bakmak, gereksiz render zincirinin başladığı noktayı bulmada Profiler'ın en güçlü katkısıdır.
Chrome DevTools Performance paneli ile döngüyü yakalamak
React Profiler React iç yapısına odaklanırken Chrome DevTools Performance paneli tarayıcı düzeyinde görür: JavaScript yürütme süresi, main thread blokajları, layout ve paint maliyetleri. Bir effect döngüsü tarayıcı katmanında da iz bırakır; iki araç birbirini tamamlar.
Performance kaydı başlatılır, kısa süre beklenir veya sorunlu etkileşim tetiklenir, kayıt durdurulur. Flame chart'ta sürekli tekrarlayan, birbirini izleyen küçük JavaScript görevleri döngünün görsel karşılığıdır. Her görev yaklaşık aynı boyuttaysa ve milisaniyeler aralıkla art arda diziliyorsa kayıt büyük olasılıkla bir render-effect döngüsünü göstermektedir. Görevlerin arasındaki boşluk React'in scheduler döngü aralığını yansıtır.
Network sekmesiyle birlikte kullanmak, fetch döngüsünü daha net ortaya çıkarır. Effect içinde bir API çağrısı varsa ve döngüye girildiyse Network sekmesi saniyede birden fazla aynı isteği gösterir. Request'lerin zaman damgaları arasındaki tutarlı küçük aralık, döngünün frekansını yaklaşık olarak verir. Bu veriyle Profiler kaydındaki render sayısı karşılaştırılabilir; ikisi birbirine yakınsa kaynak daha net konumlanır.
Memory sekmesi ek bilgi sunar. Döngü sırasında bellek sürekli artıyorsa cleanup fonksiyonu eksik olabilir ya da her effect çalışmasında yeni bir kaynak oluşturuluyordur. Event listener veya timer birikmesi bu yolla görünür hale gelir. Cleanup fonksiyonu doğru yazılmış bir effect döngüye girseydi bile bellek artışı olmaz; Memory profiling bunu doğrular ve sorunun döngü mü yoksa kaynak sızıntısı mı olduğunu ayırt ettirir.
useMemo, useCallback ve bağımlılık dondurma stratejileri
Nesne veya fonksiyon bağımlılığı zorunluysa referansı sabitlemenin yolu useMemo ve useCallback'tir. useMemo bir nesnenin referansını, bağımlılıkları değişmediği sürece aynı tutar. useCallback aynı şeyi fonksiyonlar için yapar. Effect bağımlılığına bu sarmalayıcılardan geçmiş değerleri eklemek, referans dengesizliğini ortadan kaldırır.
Her nesneyi useMemo ile sarmak çözüm değildir. Her useMemo hafızada bir karşılaştırma maliyeti taşır; basit nesneler için bu maliyet, gereksiz render maliyetinden yüksek çıkabilir. Kararı bağlam belirler: nesne render döngüsü içinde mi oluşturuluyor, kaç kez kullanılıyor, bağımlılık içeren bir effect var mı? Bu soruların cevabı "evet" ise memoization değer taşır; aksi halde erken optimizasyon olur.
Bağımlılık dizisinin dışına çıkmak da geçerli bir stratejidir. Nesne bileşen dışında tanımlanabiliyorsa, yani render'dan bağımsızsa, bağımlılık dizisine eklenmesine gerek kalmaz. Sabit konfigürasyonlar, enum benzeri nesneler bileşen dışında tanımlanabilir. Bu durumda ne useMemo gerekir ne de bağımlılık dizisi karmaşıklaşır.
Effect'in bağımlılıklarını azaltmanın bir diğer yolu, onları effect içine taşımaktır. Effect kendi içinde türetebileceği bir değeri props veya state'ten okuyorsa, bu değeri bağımlılık olarak listelemek yerine effect bloğu içinde hesaplamak mümkündür. Bu yaklaşım her zaman uygulanamaz, ama uygulanabildiğinde bağımlılık dizisini kısaltır ve dış değişimlere maruziyeti azaltır. Bağımlılık sayısı azaldıkça "bu dizide bir şey gereksiz mi?" sorusunu yanıtlamak da kolaylaşır.
Bağımlılık dizisi hatalarının büyük bölümü linter kuralları etkin tutulduğunda derleme aşamasında yakalanır. eslint-plugin-react-hooks paketinin exhaustive-deps kuralı, effect içinde kullanılan her değerin bağımlılık dizisinde yer almasını zorunlu kılar. Bu kural zaman zaman zahmetli gelir; özellikle büyük effect bloklarında uzun bağımlılık listeleri üretir. Ama bu uzunluk çoğunlukla kodu yeniden düzenleme sinyalidir: çok şeye bağımlı bir effect, büyük olasılıkla birden fazla iş yapıyordur ve ayrılabilir.
Runtime'da sorun yakalamak için React Profiler ve Chrome DevTools Performance paneli yeterli bir ikili oluşturur. Profiler render sıklığını ve bileşen ağacını gösterir; Performance paneli tarayıcı görev dağılımını ve ağ aktivitesini gösterir. İkisi birlikte kullanıldığında bir effect döngüsünü hem React katmanında hem tarayıcı katmanında izlemek mümkün olur. Sorunun hem kodda hem araç verisinde doğrulanması, aynı hatanın farklı biçimde geri dönmesini önler.
Bağımlılık dizisi, React'in en sık yanlış anlaşılan mekanizmalarından biridir; çünkü yanlış kullanım hemen görünür hata vermez. Erken yakalamak için linter kuralları, döngü veya sıklık sorunlarını bulmak için araç desteği gerekir. İkisini birlikte kullanmak, effect kaynaklı performans sorunlarını hem önler hem de ortaya çıktığında hızlıca çözüme taşır.