Intersection Observer API, tarayıcıya "şu eleman ekrana girince bana haber ver" demenin yerleşik yolu. Scroll event'e dokunmadan, her kaydırmada layout hesabı tetiklemeden çalışan bir yapı. Bu "yerleşik" kelimesi, API'nin hatasız kullanıldığı anlamına gelmiyor; aksine, yanlış yapılandırıldığında sessizce hatalı davranan, belleği tutmaya devam eden ve layout bütçesini tüketen bir gözlemci ortaya çıkabiliyor.

rootMargin hatalı yazılmış bir observer, elemanın görünüp görünmediğini yanlış raporlar. Threshold seçimi gevşek tutulursa callback beklenenden çok daha sık tetiklenir. Observer nesnesi kullanıldıktan sonra bellekten serbest bırakılmazsa, DOM'dan kaldırılan elemanlara hâlâ referans tutulur. Bu üç hata birbirinden bağımsız gibi görünse de birlikte yaşandıklarında sayfanın scroll performansını ve bellek kullanımını ölçülür biçimde etkiler.

Üç hata ayrı belirtiler üretir. Hangisinin sorun yaratmadığı, hangisinin kritik hale geldiği, eleman sayısı ve oturum süresiyle birlikte değişir.

rootMargin Neden Beklediğiniz Gibi Çalışmaz

rootMargin, observer'ın izlediği kökün sınırlarını genişletmek veya daraltmak için kullanılan bir CSS margin dizisi. Değer "100px 0px 0px 0px" olarak yazıldığında viewport'un üstüne 100px eklenir; eleman viewport'a 100px kalmadan gözlemci tetiklenir. Mantıklı görünür. Ama bu değerin davranışı root parametresine göre önemli ölçüde değişir.

root: null (varsayılan) kullanıldığında rootMargin viewport'a göre hesaplanır. root olarak bir kaydırılabilir kapsayıcı belirtildiğinde ise margin, o kapsayıcının sınırlarına göre uygulanır. Aynı rootMargin değeri, farklı root seçimiyle farklı bir tetikleme noktası üretir. Bu ayrım, "eleman neden çok erken ya da çok geç görünüyor?" sorularının büyük bölümünün kaynağı.

Bir başka sessiz hata: rootMargin yalnızca px veya % birimini kabul eder. em veya vw yazıldığında gözlemci sessizce varsayılan değerle devam eder ya da hiç çalışmaz. Konsol uyarısı üretilmez, hata fırlatılmaz. Observer tanımlanmış gibi görünür; eleman hiçbir zaman tetiklenmez. % birimi kullanıldığında da dikkat gereken bir ayrıntı var: bu değer, root'un genişliğine göre hesaplanır. Hem yatay hem dikey kenarlar, root'un yatay boyutuna oranlanır; yani "10% 0px" yazan satır, root yüksekliğine değil genişliğine bakarak üst ve alt mesafeyi hesaplar.

rootMargin negatif değer de alabilir: "-50px" yazıldığında viewport içine doğru daralır, elemanın gerçekten ekranın bir bölümüne girmesi beklenir. Bu değeri dinamik olarak hesaplayıp her resize olayında observer'ı yeniden oluşturursanız ve her yeniden oluşturma önceki observer'ı açıkça kapatmazsanız, bellekte birden fazla aktif gözlemci birikir. Yeniden oluşturma, temizleme değil; ekleme yapar.

Threshold Seçimi: 0 ile 1 Arasında Ne Fark Var

threshold: 0, elemanın tek bir pikseli viewport'a girdiği an callback'i tetikler. Çoğu kullanım için bu yeterlidir. Ama "eleman viewport'a girdi" ile "eleman gerçekten okunabilir konumda" farklı kavramlar; kullanıcının görmeden hızla geçtiği, yalnızca köşeden giren bir eleman da bu eşiği geçer ve tetikleme gerçekleşir.

threshold: 1.0 ise elemanın tamamının viewport içinde olmasını bekler. Eleman viewport yüksekliğinden uzunsa bu eşiğe hiçbir zaman ulaşılamaz, callback çalışmaz. Sayfada tam ekran bir hero bölüm veya uzun bir tablo varsa, threshold: 1.0 ile oluşturulan gözlemci sessiz kalır ve bu hata kendini belli etmez.

Birden fazla eşik dizisi de verilebilir: [0, 0.25, 0.5, 0.75, 1]. Her eşik geçişinde callback tetiklenir. Analitik izleme için uygun; animasyon veya lazy load için kullanıldığında gereksiz callback sayısını artırır. Her tetiklenme callback içindeki kodu çalıştırır; o kod DOM'u sorguluyor ya da bir şey yazıyorsa her eşik geçişi ek bir maliyet üretir.

Threshold değeri sabit bir tercih değil, elemanın beklenen görünürlük davranışına göre seçilen bir parametre. Dar viewport'ta 0.5 eşiği mantıklı çalışırken büyük ekranda aynı eşik farklı bir tetikleme noktası üretir. Elemanın yüksekliği viewport yüksekliğine yaklaştığında orta eşikler beklenmedik biçimde davranır; bu nedenle threshold seçimi, gerçek kullanım senaryosunda test edilmeden kesinleştirilmemeli.

Observer disconnect Edilmezse Ne Tutar

disconnect() çağrılmadı.

IntersectionObserver nesnesi oluşturulduktan sonra, gözlemlenen elemanlara referanslar bellekte canlı kalır. Eleman DOM'dan kaldırılsa bile observer o elemana bağlı kalmaya devam eder ve tarayıcı bu nesneyi garbage collector'a teslim edemez. Tek bir sayfa için bu küçük görünebilir; SPA içinde rotadan rotaya geçilirken her geçişte yeni observer'lar oluşturulur ve hiçbiri kapatılmazsa bellekte biriken referans listesi oturum süresince büyümeye devam eder.

Chrome DevTools'un Memory sekmesinde heap snapshot alındığında, IntersectionObserver nesnelerinin sayısının oturum boyunca arttığı görülür. Her biri küçük, ama sonsuz liste, feed veya çok sayıda kart render eden arayüzlerde bu birikim dakikalar içinde fark edilir hale gelir. Observer'ın tuttuğu referans zinciri, callback fonksiyonu üzerinden kapsamdaki (closure) değişkenlere de uzanabilir; bu da beklentiden daha geniş bir bellek alanının tutulmasına yol açar.

Doğru kullanım: eleman artık izlenmeyecekse observer.unobserve(element) çağrılır; observer tamamen kapatılacaksa observer.disconnect() yeterlidir. React'ta bu, useEffect'in cleanup fonksiyonuna yazılır:

useEffect(() => {
  const observer = new IntersectionObserver(callback, options);
  observer.observe(ref.current);
  return () => observer.disconnect();
}, []);

Cleanup unutulduğunda bileşen unmount olsa da observer yaşamaya devam eder. ref.current null olduğunda callback çalışmayabilir, ama observer nesnesi bellekte kalmayı sürdürür. React Strict Mode geliştirme ortamında bileşenleri iki kez mount ettiğinden, cleanup eksikliği orada daha hızlı fark edilir; ama bu davranışa yaslanmak yerine kapatma mantığını her zaman açıkça yazmak daha güvenli.

Callback İçinde DOM Ölçümü ve Layout Thrashing

Observer callback'i ana thread'de çalışır. Callback tetiklendiğinde DOM'u sorgularsanız tarayıcı layout hesabını yeniden çalıştırmak zorunda kalabilir; sık tetiklenen bir observer için bu önemli bir sorun.

Layout thrashing şöyle oluşur: callback içinde element.getBoundingClientRect() veya element.offsetTop okunur; bu okuma, son layout'tan bu yana DOM değişmişse tarayıcıyı zorla layout yapmaya iter. Ardından başka bir elemanın boyutu değiştirilirse, sonraki okuma yeniden layout tetikler. Okuma-yazma-okuma-yazma döngüsü her frame'de tekrarlandığında frame süresi uzar ve sayfa takılır.

Intersection Observer, scroll event'in neden olduğu bu sorunu çözmek için tasarlandı; scroll event yerine kullanılmasının temel nedeni budur. Ama callback içinde harici position veya boyut sorgusu yapıldığında kazanılan avantaj geri alınır. Callback parametresi olarak gelen IntersectionObserverEntry nesnesi, boundingClientRect, intersectionRect ve rootBounds bilgilerini zaten taşır; bunlar için DOM'u yeniden sorgulamaya gerek yoktur.

Callback'te yapılması gereken iş mümkün olduğunca küçük tutulmalı: durumu güncelle, gerekirse bir CSS sınıfı ekle, sonra bitir. DOM'da okuma yapılacaksa entry'nin hazır sunduğu veriler kullanılmalı; bunlar yetmiyorsa okuma ve yazma işlemleri requestAnimationFrame içinde gruplandırılmalı.

Tek Observer mu, Çok Observer mu

Sayfada onlarca eleman izlenecekse, her eleman için ayrı bir IntersectionObserver örneği oluşturmak yaygın bir hatadır. Her observer kendi iç veri yapısını, kendi referans listesini taşır. Aynı seçeneklerle (rootMargin, threshold) çalışan gözlemciler varsa bunları tek bir observer altında toplamak hem bellek hem de yönetim açısından daha temizdir:

const observer = new IntersectionObserver(callback, { rootMargin: "0px", threshold: 0.1 });
elements.forEach(el => observer.observe(el));

Callback içinde hangi elemanın tetiklendiği entry.target ile ayırt edilir. Bu yapı, yüzlerce kart veya liste öğesi izlendiğinde observer sayısını tekle indirir.

Her gözlemcinin farklı rootMargin veya threshold değerine ihtiyacı varsa bunları aynı observer altında toplamak mümkün değildir; bu durumda birden fazla observer zorunludur. Seçenekler ortaksa tek observer, seçenekler farklıysa ayrı observer mantığı geçerlidir. Karar çok observer kullanmak değil, gereksiz yere çoğaltmamak.

Bir Kez Tetiklenince Yeter: unobserve Zamanlaması

Lazy load, animasyon tetikleme veya analitik "ilk görünüm" sayacı gibi kullanımlarda observer'ın elemanı yalnızca bir kez yakalaması yeterlidir. Eleman görünüp kaybolduğunda tekrar tekrar callback çalışmasına gerek yoktur. Bunun için callback içinde tetiklenen elemana unobserve çağrılır:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadImage(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

Bu kalıp kullanılmadığında, kullanıcı aşağı inip yukarı çıktığında her geçişte callback yeniden çalışır. Görüntü yükleme gibi işlemler bundan etkilenmez çünkü görüntü zaten yüklenmiştir; ama veri fetch, animasyon başlatma veya event gönderme gibi işlemler fazladan çalışarak hata üretebilir ya da analitik verilerini bozabilir. "Bir kez yetişir" mantığıyla çalışan her kullanım unobserve ile kapatılmalıdır.

Tam tersi bir hata da var: lazy load kurulurken observer'ı çok erken unobserve etmek. Görüntü yüklenmeden önce unobserve çağrılırsa ve ağ gecikmesi nedeniyle görüntü henüz gelmemişse, öğe ekrandan kaybolup tekrar girdiğinde artık tetiklenme olmaz ve görüntü hiç yüklenmez. Unobserve, işlem tamamlandıktan sonra çağrılmalıdır.

iframe ve Cross-Origin Kısıtlamaları

Intersection Observer'ın sessizce farklı davrandığı bir alan daha var: iframe içindeki elemanlar. Aynı origin'den gelen iframe'lerde observer beklediği gibi çalışır. Cross-origin iframe'de ise gizlilik kısıtlamaları nedeniyle bazı IntersectionObserverEntry değerleri sıfır döndürür veya tamamen boş gelir; boundingClientRect, rootBounds ve intersectionRect bu kategoride. Eleman görünüyor gibi davranır, callback tetiklenir, ama koordinat verileri güvenilmez.

Reklam birimleri, gömülü video oynatıcıları veya üçüncü taraf widget'lar çoğunlukla cross-origin iframe içinde gelir. Bu elemanlar için görünürlük izleme yapılıyorsa gelen verinin eksik olabileceği göz önünde tutulmalıdır. Visibility API veya üçüncü tarafın kendi sağladığı callback mekanizması bu bağlamda daha güvenilir bir yol olabilir.

Elemanın iframe içinde mi, dışında mı olduğu her zaman açık değildir; özellikle dinamik olarak yerleştirilen bileşenlerde bu kontrol atlanır. Observer kurulmadan önce hedef elemanın hangi document'e ait olduğunu doğrulamak, belirsiz davranışların önüne geçer.

Doğru kurulmuş bir observer layout hesabını tetiklemeden görünürlük bilgisi üretir; olay dinleyicisi gerekmez, tarayıcı hesabı kendi zamanlamasıyla yapar. Kazanç, rootMargin ve threshold bilinçli seçildiğinde, observer kullanıldıktan sonra kapatıldığında ve callback içindeki iş minimum tutulduğunda kalır.

API iki satırla kurulur. rootMargin bir piksel yanlış yazıldığında tetikleme noktası kayar; threshold liste için seçilmeden bırakıldığında gereksiz işlem üretir; disconnect unutulduğunda bellek serbest kalmaz. Küçük sayfada fark görünmeyebilir; sonsuz scroll, büyük liste veya uzun oturumda her biri ölçülür bir etkiye dönüşür.