Kullanıcının tarayıcı sekmesi otuz dakika sonra donuyor. Arayüz (UI) takılıyor. Ardından tarayıcı, bellek yetersizliği (out-of-memory) hatasıyla çöküyor.

Bileşen kodunu satır satır inceliyorsunuz ve hiçbir sorun görmüyorsunuz. React'teki bellek sızıntılarının (memory leaks) çıldırtıcı yanı da budur. Hata, JSX sözdiziminizde veya hook mantığınızda değil; bileşeniniz ile tarayıcının çöp toplayıcısı (garbage collector) arasındaki boşlukta yer alır. Başıboş bir olay dinleyicisi (event listener) veya uzun ömürlü bir closure, toplayıcının belleği geri kazanmasını engeller. Bir bileşeni unmount edersiniz, ancak tek bir kalıcı referans tüm ağacı heap içinde canlı tutar. Bu sızıntıları sadece kodu okuyarak bulamazsınız. Sorun yüzeyin altında gizli kalır; gözle görülmez ve birim testlerinde (unit tests) sessizdir.

Sızıntılar Neden Göz Önünde Gizlenir?

V8 gibi JavaScript motorları belleği otomatik olarak yönetir. Kökten (root) bir nesneye giden hiçbir referans yolu kalmadığında, motor o nesneyi çöp olarak işaretler ve alanı geri kazanır. Bu süreç, gizli bir referans planladığınızdan daha uzun süre hayatta kaldığı ana kadar iyi çalışır.

React'te tehlike genellikle bileşenler ve DOM arasındaki sınırda ortaya çıkar. Bir modal içinde window nesnesine bir resize dinleyicisi ekleyebilir veya bir dashboard widget'ında bir WebSocket'e abone olabilirsiniz. Kullanıcı modal'ı kapattığında veya başka bir sayfaya geçtiğinde bileşen unmount edilir. Eğer abonelik devam ederse, motor global window nesnesinden sizin handler'ınıza, handler'ınızdan da bileşenin closure'ına kadar giden geçerli bir referans görür. Bileşen, prop'ları, state'i ve DOM düğümlerinden oluşan tüm alt ağacı bellekte sabitlenmiş (pinned) olarak kalır. Yüzlerce etkileşim boyunca bu sabitlenmiş nesneler birikir. Bellek kullanımı, asla tam olarak eski seviyesine düşmeyen testere dişi (sawtooth) şeklinde bir desenle yükselir.

Kurumsal Dashboard Problemi

Bu durum en çok, kullanıcıların saatlerce tek bir sayfada kaldığı kurumsal dashboard'larda gerçekleşir. İzleme panellerini, analitik görünümlerini veya biletleme (ticketing) sistemlerini düşünün. Bir kullanıcı detay modal'ı açar, büyük bir veri setini filtreler veya tek sayfalık bir uygulama (SPA) içinde sekmeler arası geçiş yapar. Her bir etkileşim normal hissettirir. Ancak zamanla, sahipsiz (orphaned) düğümler ve ayrılmış (detached) dinleyiciler birikir. Uygulama, tek bir maliyetli render nedeniyle değil, heap yeterince büyüyüp sık ve maliyetli çöp toplama duraklamalarını (garbage collection pauses) tetiklediği için yavaşlar.

useEffect hook'larınız hakkında tahmin yürütmeyi bırakın. Bir sızıntının olup olmadığını bilmenin tek yolu heap'i doğrudan ölçmektir. Chrome DevTools size bu görünürlüğü sağlar.

Chrome DevTools ile Sızıntı Avı

Yeniden üretilebilir bir dizi işleme ve birkaç dakikalık odaklanmış dikkate ihtiyacınız var. Uygulamanızı Chrome'da açın, DevTools'u başlatın ve Memory sekmesine gidin.

Bir temel (baseline) kaydedin. Heap snapshot seçeneğini belirleyin ve Take snapshot butonuna tıklayın. Bu, şu anda JavaScript heap'inde yaşayan her nesneyi yakalar ve size başlangıç belleğini gösterir. Bunu sayfa ilk yükleme sırasında değil, sayfa ilk boşta (idle) durumuna yerleştikten sonra yapın; böylece yalnızca kullanıcı eylemlerinin neden olduğu büyümeyi ölçersiniz.

Eylemi tetikleyin. Sızıntıya neden olduğundan şüphelendiğiniz tam UI etkileşimini gerçekleştirin. Bir modal'ı açıp kapatın, karmaşık bir grafiği açıp kapatın veya bir rotayı değiştirip geri dönün. İşlem bittiğinde uygulamayı orijinal görsel durumuna döndürün. Bu adım çok önemlidir. UI'ın, temel (baseline) sırasında göründüğüyle tamamen aynı görünmesini istersiniz. UI boş görünmesine rağmen heap büyüdüyse, elinizde güçlü bir sızıntı kanıtı var demektir.

Çöp toplamayı zorlayın. Memory sekmesindeki çöp kutusu simgesine tıklayın. Bu, tam bir GC döngüsünü tetikler ve bir sonraki toplama işlemine kadar meşru bir şekilde hayatta kalan geçici nesneleri temizler. Geriye kalanlar gerçek sızıntılardır; toplanmış olması gereken ancak kazara referanslar nedeniyle canlı tutulan nesnelerdir.

İkinci bir snapshot alın. Tekrar Take snapshot butonuna tıklayın. Artık aynı UI koşulları altında alınmış iki heap fotoğrafına sahipsiniz.

Sonuçları karşılaştırın. Görünümü Summary'den Comparison'a değiştirin. Baseline olarak ilk snapshot'ınızı, karşılaştırılacak snapshot olarak ise ikincisini ayarlayın. Comparison görünümü her nesne kategorisini listeler ve size delta'yı, yani iki yakalama arasındaki nesne sayılarındaki net değişimi gösterir.

Delta'ya göre sıralayın. Önemli ölçüde artan kategorileri arayın. Özellikle Detached HTMLElement ve React fiber düğümlerine odaklanın. Ayrılmış (detached) bir HTML öğesi, artık aktif belge ağacına bağlı olmayan ancak bazı JavaScript referanslarının hala onu tuttuğu bir DOM düğümüdür. Bunlar kesin kanıtlardır. Bir modal'ı kapattıktan veya bir bileşeni unmount ettikten sonra bunların var olmaması gerekir.

Retaining Path'i Okumak

Snapshot'ta ayrılmış (detached) bir öğe seçtiğinizde, Chrome alt panelde tutma yolunu (retaining path) gösterir. Bu yol, kökten seçilen nesneye kadar uzanan bir referans zinciridir. Onu dikkatlice takip edin. Genellikle bir olay dinleyicisi (event listener), bir IntersectionObserver, bir setInterval kimliği (ID) veya bileşeninizdeki belirli bir satırı işaret eden bir closure bulacaksınız.

Tanıdığınız isimleri arayın. Eğer window nesnesine bağlı, kod tabanınızdan gelen bir fonksiyon ismine sahip bir dinleyici görürseniz, çapa noktasını (anchor) bulmuşsunuz demektir. O dinleyiciyi tutan nesne, tüm bileşeninizin hayatta kalmasını sağlıyor. Bazen zincir üçüncü taraf bir kütüphane üzerinden geçer. Bu durumlarda, kütüphanenin bir temizleme (cleanup) fonksiyonu içinde çağırmayı unuttuğunuz açık bir yıkım (teardown) çağrısı bekleyip beklemediğini kontrol edin.

Kök Nedenleri Düzeltme

Tutma yolunu belirledikten sonra, çözüm genellikle mekaniktir ancak ekip genelinde disiplin gerektirir.

Temizleme (cleanup) fonksiyonları kullanın. window veya document nesnelerine dinleyiciler eklediğinizde, useEffect içinde her zaman bir temizleme fonksiyonu döndürün. Eğer effect'iniz bir boyutlandırma (resize) olayına abone oluyorsa, bileşen unmount olmadan önce bu aboneliği kaldırın. Temizleme işlemi React bileşeni yıktığında (teardown) çalışır ve bu da size harici bağlantıları kesmek için garantili bir kanca (hook) sağlar.

Referansları sabitleyin. Handler'larınızı useCallback ile sarmalayın. Bu, addEventListener ile orijinalde geçtiğiniz fonksiyon referansının aynısını removeEventListener fonksiyonuna da geçmenizi sağlar. Eğer window.addEventListener('resize', () => { ... }) gibi satır içi (inline) bir fonksiyon kaydederseniz ve daha sonra bunu başka bir satır içi fonksiyonla kaldırmaya çalışırsanız, referanslar eşleşmeyecektir. Dinleyici bağlı kalmaya devam eder. İçindeki closure ise bileşen durumunuzu (state) hayatta tutar. Kararlı bir bağımlılık dizisine (dependency array) sahip useCallback, bu kimlik uyuşmazlığını önler.

Global bağlantıları kesin. window ve document gibi tarayıcının olay hedefi (event target) nesnelerinin sayfanın ömrü boyunca yaşadığını unutmayın. Bunlardan bileşeninize giden herhangi bir referans, global bir çapa görevi görür. Dinleyiciyi kaldırmak bu çapayı kırar ve V8 motorunun bir sonraki çöp toplama (garbage collection) döngüsünde bileşen durumunu ve DOM düğümlerini temizlemesine olanak tanır.

Pencere genişliğini takip eden bir modal düşünün. Temizleme işlemi olmazsa, kullanıcı modalı her açtığında yeni bir dinleyici eklenir. Eski dinleyiciler asla ayrılmaz çünkü ait oldukları bileşen örnekleri (instances) yok olmuştur, ancak fonksiyonların kendileri anonimdir ve kaybolmuştur. useCallback ile sarmalanmış isimlendirilmiş bir handler ve removeEventListener çağıran bir temizleme fonksiyonu kullanmak, bu döngüyü düzgün bir şekilde kapatır.

Gerçek Bir Çıkarım

React'teki bellek sızıntıları (memory leaks) nadiren net bir hata mesajıyla kendilerini belli ederler. Kendilerini, açık kaldığı süre boyunca ağırlaşan bir sekme ile belli ederler. Hangi hook'un suçlu olduğu konusunda spekülasyon yaparak vakit kaybetmeyin. Memory sekmesini açın, çöp toplamayı (garbage collection) zorlayın ve snapshot'ları karşılaştırın. Heap profiler'ın size tam tutma yolunu göstermesine izin verin. Ardından temizleme fonksiyonunu yazın, callback referansını sabitleyin ve global bağlantıyı kesin. Kullanıcılarınız düzeltmeyi doğrudan fark etmeyecektir ancak günün sonunda panelin hala sorunsuz çalıştığını fark edeceklerdir.