Üretimde bir sekme üç saat sonra yavaşlıyor ama yerel ortamda hiç olmuyorsa, aradığınız yer kullanıcının makinesi değil; kodun içinde asılı kalmış bir referanstır. Bellek sızıntısı JavaScript'te çöp toplayıcının başarısızlığı değil, bizim çöpü bırakmayı reddetmemizdir. Toplayıcı yalnızca kendisine hiçbir yoldan ulaşılamayan nesneleri toplayabilir; siz bir nesneyi hâlâ işaret ediyorsanız, o nesne bellekte bir kiracı gibi oturmaya devam eder.

Sızıntının Dört Klasik Kapısı

İlk şüpheliler her projede aynıdır: kaldırılmayan olay dinleyicileri, kapatılmayan zamanlayıcılar, kapanış içinde tutulan büyük veriler ve global değişkenlere fazladan bağlanan nesneler. En sık gördüğümüz örnek şudur:

function sekmeyiBaslat() {
  const rapor = buyukVeriSetiniYukle(); // 40 MB'lik dizi
  return setInterval(function yaz() {
    console.log(rapor.toplam);
  }, 1000);
}

Bu zamanlayıcı durdurulmadığı sürece 40 MB'lik dizi hiç toplanamaz; sekme arka planda uyusa bile bellek yer. Çözüm, clearInterval'ı bileşen sökülürken çağırmak ve bunu bir temizlik fonksiyonuna bağlamaktır. Aynı hesap olay dinleyicileri için de geçerlidir: removeEventListener çağrılmayan her kayıt, sökülmüş bir sayfada hayalet olarak yaşamaya devam eder. Kapanış içinde tutulan büyük veri de aynı mekanizmayla korunur: fonksiyon döndükten sonra bile o değişkene işaret eden tek bir referans kalmışsa, toplayıcı elini uzatamaz.

Performance Paneliyle Avlanmak

Chrome DevTools'un Memory paneli bu işin mikroskobudur. Heap snapshot al, normal gezin; ikinci ve üçüncü snapshot arasında büyüyen dizileri bul, karşılaştırma görünümünde "Objects allocated between" filtresiyle suçluyu yakala. Retention zincirini okuduğunda genellikle tek bir düğüm seni köke götürür: bir dinleyici, bir zamanlayıcı ya da temizlenmeyen bir önbellek.

const onbellek = new Map();
function onbellegeEkle(anahtar, deger) {
  onbellek.set(anahtar, deger); // kimse temizlemiyor
}

Sınırsız büyüyen her Map, er ya da geç sızıntıya dönüşür. Çözüm ya kapasite sınırı koymak ya da anahtarın yaşam döngüsünü izlemektir. Üretimde ise sayfa görünmez olduğunda visibilitychange dinleyicisiyle ölçüm kaydedin; performance.memory en azından bir eğim verir. Ölçümü seyrek örneklemlerle toplamak, izleme kodunun kendi kendine sızıntı üretmesini de önler.

Zayıf Referanslar

Modern çözüm WeakRef ve WeakMap'ten geçer: anahtar nesne yok olduğunda değer de toplanabilir olur; önbellek anahtarlarını bu zayıf haritalarda tutmak çoğu sızıntıyı baştan önler. Ama bunlar birer zarafettir; asıl çözüm çoğu zaman yaşam döngüsünü doğru yerde kapatmaktır. useEffect'te dönüş fonksiyonu, disconnectedCallback ve yönlendirici geçişlerindeki temizlik hook'ları bu savaşın asıl silahlarıdır. Kural basit: bir kaynak açtıysanız, kapanışının da adresini önceden yazın. Kod incelemesinde sorulacak en iyi soru da budur: "bu nesne kimin ömrüne bağlı ve o ömür bittiğinde referansı bırakacak bir satır var mı?" Bu soruyu her çekme isteğinde sormaya başlayan ekiplerin üretimdeki bellek eğimleri bir yıl sonra belirgin biçimde düzleşir. Avcılık, refleks değil rutin işidir; rutinleştirildiğinde de sızıntı avdan çıkmış olur. Son tuzak, aracın kendisine aşırı güvenmektir: otomatik çözümleyiciler dinleyici ve zamanlayıcı sızıntılarının çoğunu bulur ama kapanış içinde saklanan büyük veriyi yalnızca snapshot karşılaştırması ele verir. Araçları çalıştırın, ama retention zincirini okumayı da öğrenin; sızıntı avcılığının özeti üç fiildir: ölç, karşılaştır, bırak.