Bir elemanın ekranda görünüp görünmediğini öğrenmenin en kolay ve en verimli yolu IntersectionObserver kullanmaktır. Gözlemciyi bir geri çağırma fonksiyonu ve isteğe bağlı ayarlarla oluşturursunuz, observe(eleman) ile hedefi verirsiniz; eleman görüş alanına girip çıktıkça ya da görünen oranı belirlediğiniz eşikleri geçtikçe tarayıcı fonksiyonunuzu çağırır. Resimleri tembel yüklemek (lazy load), sonsuz kaydırmada yeni içerik getirmek veya bir bölüm görününce animasyon başlatmak bu API ile yapılır.

Bu yazıda root, rootMargin ve threshold ayarlarının tam olarak neyi değiştirdiğini, IntersectionObserverEntry nesnesinin alanlarını, ilk çağrının neden hemen geldiğini, iki pratik desen (tembel resim yükleme ve sonsuz kaydırma) ile sık yapılan hataları göreceksiniz. Örnekler MDN belgelerine göre yazıldı; tarayıcıda çalışan kodlar olduğu için çıktı yorumu yalnızca hesaplama yapan saf JavaScript parçasında var.

Neden scroll olayı yerine IntersectionObserver?

Eskiden görünürlüğü anlamak için scroll olayına dinleyici ekler, her seferinde getBoundingClientRect() çağırıp koordinatları kendiniz karşılaştırırdınız. MDN bu yaklaşımın sorununu açıkça anlatır: bu kodların hepsi ana iş parçacığında (main thread) çalışır ve sayfada birkaç kütüphane aynı anda kendi kontrolünü yapıyorsa kaydırma sırasında sürekli tetiklenip sayfayı yavaşlatır. Intersection Observer API ise kesişim takibini tarayıcıya bırakır. Siz yalnızca hangi durumda haber almak istediğinizi söylersiniz, tarayıcı da bu hesabı kendi uygun gördüğü şekilde optimize eder.

API'nin yapamadığı bir şey de vardır: tam olarak kaç pikselin örtüştüğüne ya da hangi piksellerin göründüğüne göre mantık kuramazsınız. API, "hedef yaklaşık yüzde N oranında göründüğünde bir şey yap" türündeki yaygın ihtiyaca çözüm üretir. Piksel düzeyinde kesinlik gerekiyorsa bu araç doğru seçim değildir.

Temel kullanım: gözlemci oluşturma ve hedef verme

new IntersectionObserver(callback, options) çağrısı bir gözlemci döndürür. Geri çağırma iki parametre alır: entries (bir IntersectionObserverEntry dizisi) ve gözlemcinin kendisi. Ardından izlemek istediğiniz her eleman için observe() çağırırsınız. Tek gözlemci istediğiniz kadar hedefi izleyebilir; hepsi aynı kök ve eşik ayarlarını paylaşır.

javascript
const options = {
  root: null,          // null: tarayıcının görüş alanı (viewport)
  rootMargin: "0px",
  threshold: 0.5,      // hedefin yarısı göründüğünde
};

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add("active");
    } else {
      entry.target.classList.remove("active");
    }
  });
}, options);

document.querySelectorAll(".box").forEach((el) => observer.observe(el));

Burada her .box elemanı görüş alanına en az yarısı girdiğinde active sınıfını alır, çıktığında kaybeder. Sınıf ekleme ve kaldırma ayrıntıları classList ve setAttribute yazısında, eleman seçme yöntemleri querySelector rehberinde anlatılıyor.

Ayarlar: root, rootMargin, threshold

Ayarlar: root, rootMargin, threshold: root ve kök dikdörtgeni, rootMargin ile önceden yükleme, threshold ve eşik listesi
AyarVarsayılanNe yapar
rootnull (viewport)Kesişimin ölçüleceği ata eleman. Hedef bu elemanın soyundan olmalı.
rootMargin0px 0px 0px 0pxKök kutusunu her yönde büyütür (pozitif) veya küçültür (negatif). Yalnızca px ve % kabul edilir.
threshold0Tek sayı ya da 0.0 ile 1.0 arası sayılardan oluşan dizi. Geri çağırmanın hangi görünürlük oranlarında çalışacağını belirler.
scrollMargin0px 0px 0px 0pxİç içe kaydırma kaplarının kırpma dikdörtgenine eklenen pay; rootMargin ile aynı söz dizimi.

root ve kök dikdörtgeni

root boş bırakılırsa ya da null verilirse kesişim, en üst belgenin görüş alanına göre hesaplanır. Bir eleman verirseniz o eleman hedefin atası olmak zorundadır. MDN kök dikdörtgeninin nasıl bulunduğunu üç durumda açıklar: kök örtük kök ise (üst düzey Document) görüş alanının dikdörtgeni kullanılır; kökte bir taşma kırpması (overflow clip) varsa kökün içerik alanı kullanılır; aksi halde kökün getBoundingClientRect() ile dönen kutusu kullanılır. Kaydırılabilir bir liste kutusu içindeki elemanları izliyorsanız root olarak o kutuyu vermeniz gerekir; aksi halde gözlemci sayfa görüş alanına göre çalışır.

rootMargin ile önceden yükleme

rootMargin, CSS margin özelliğine benzeyen bir ile dört değerli bir metindir ve kök kutusunun her kenarına eklenir. Pozitif değer kutuyu büyütür; böylece hedef henüz ekranda değilken bile kesişiyor sayılır. MDN'nin önerdiği kullanım tam olarak budur: resimleri görünür olduklarında değil, görünmelerinden biraz önce yüklemeye başlamak. Negatif değer ise kutuyu küçültür ve hedef ekranda görünmeye başlasa bile kesişim olarak kabul edilmez. Değerlerde yalnızca px ve % birimleri kullanılabilir; geçersiz bir rootMargin veya scrollMargin verirseniz yapıcı SyntaxError türünde bir DOMException fırlatır.

Okuma tarafında dikkat edilecek bir ayrıntı var: observer.rootMargin özelliği, sizin yazdığınız metni birebir geri vermeyebilir. MDN'ye göre dönen değer her zaman dört bileşen içerir; siz "200px" yazsanız bile dört kenarlı biçimde okursunuz.

threshold ve eşik listesi

threshold, hedefin görünen alanının toplam sınır kutusuna oranıdır. 0 (varsayılan) hedef kökün sınırına değdiği anda tetiklenir; 1.0 ise hedefin her pikseli görünmeden eşiğin geçildiği sayılmaz. Dizi verirseniz, görünürlük her eşiği ileri ya da geri geçtiğinde ayrı bildirim alırsınız. 0.0 ile 1.0 aralığı dışındaki bir değer RangeError fırlatır.

MDN'nin örneğindeki gibi eşik listesini programla da üretebilirsiniz. Aşağıdaki fonksiyon saf JavaScript olduğu için yerel Node.js ile çalıştırıldı ve çıktılar gerçek sonuçtur:

javascript
function buildThresholdList(numSteps) {
  const thresholds = [];
  for (let i = 1; i <= numSteps; i++) {
    thresholds.push(i / numSteps);
  }
  thresholds.push(0);
  return thresholds;
}

console.log(JSON.stringify(buildThresholdList(4)));  // [0.25,0.5,0.75,1,0]
console.log(JSON.stringify(buildThresholdList(10))); // [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1,0]
console.log(buildThresholdList(20).length);          // 21

Listenin sonunda 0 olması sorun değildir. MDN, gözlemcinin thresholds özelliğinin, verdiğiniz dizi hangi sırada olursa olsun her zaman küçükten büyüğe sıralı döndüğünü belirtir. Ayarda tek bir sayı verdiyseniz thresholds o sayıyı içeren tek elemanlı bir dizi olur, hiç vermediyseniz [0] olur.

IntersectionObserverEntry alanları

IntersectionObserverEntry alanları: target, isIntersecting, intersectionRatio, intersectionRect

Geri çağırmaya gelen her girdi, bir hedefin belirli bir andaki kesişim durumunu anlatır. Girdi yalnızca o anı tarif eder; kaydırma yönü veya hızı gibi zaman içinde izlenmesi gereken bilgiler için önceki girdileri kendiniz saklamanız gerekir.

AlanAnlamı
targetKesişimi değişen eleman
isIntersectingtrue ise hedef köke girmiş (kesişim durumuna geçiş), false ise kesişimden çıkmış
intersectionRatio0.0 ile 1.0 arası görünen oran: intersectionRect alanının boundingClientRect alanına oranı
intersectionRectHedefin görünen kısmının dikdörtgeni
boundingClientRectHedefin sınır kutusu
rootBoundsrootMargin uygulanmış kök kesişim dikdörtgeni
timeDeğişimin belge oluşturulduğundan bu yana milisaniye cinsinden zamanı (DOMHighResTimeStamp)

Bir köşe durumu: hedefin sınır kutusunun alanı sıfırsa intersectionRatio, isIntersecting true ise 1, değilse 0 olur. Ayrıca hedef ile kök yalnızca bir kenar çizgisini paylaşıyorsa (kesişim dikdörtgeni sıfır alanlı) MDN bunu kesişim durumuna geçiş olarak saymaz. Bu yüzden görünürlük kararlarında yalnızca intersectionRatio > 0 yerine isIntersecting değerine bakmak çoğu zaman daha anlaşılır sonuç verir.

Girdiler bir kuyruk üzerinden gönderildiği için genelde oluşturulma sırasıyla gelir, ama MDN sıralama gerekiyorsa time alanını kullanmayı önerir. Tek bir çağrıda kaç girdi geleceğini de varsaymayın: birden fazla hedef ya da kısa sürede birden fazla eşik geçen tek hedef aynı çağrıda birlikte raporlanabilir.

İlk çağrı hemen gelir

Yeni başlayanların en çok şaşırdığı davranış şudur: observe() çağrıldıktan sonraki ilk çizim döngüsünde geri çağırma her zaman çalışır, hedef hiç hareket etmemiş olsa bile. MDN'ye göre görüş alanının dışındaki bir hedef için en az bir girdi isIntersecting: false ile, içindeki bir hedef için true ile hemen gelir. Kodunuz ilk çağrıyı "kullanıcı kaydırdı" diye yorumlarsa, örneğin sayfa açılır açılmaz bir analitik olayı gönderirse yanlış veri üretirsiniz. Çözüm basittir: her girdide isIntersecting değerine bakın ve yalnızca istediğiniz geçişte işlem yapın.

Desen 1: resimleri tembel yükleme

Aşağıdaki örnekte gerçek adres data-src özniteliğinde bekler. Resim görüş alanına 200 piksel yaklaşınca adres src'ye taşınır ve aynı resim bir daha izlenmesin diye unobserve() çağrılır. data-* özniteliklerinin HTML tarafı HTML öğrenme rehberinde anlatılıyor.

html
<img class="lazy" data-src="/img/urun-1.jpg" alt="Ürün 1" width="320" height="240" />
<img class="lazy" data-src="/img/urun-2.jpg" alt="Ürün 2" width="320" height="240" />
javascript
const lazyObserver = new IntersectionObserver(
  (entries, observer) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img); // bu resim için iş bitti
    });
  },
  { rootMargin: "200px 0px" },
);

document.querySelectorAll("img.lazy").forEach((img) => lazyObserver.observe(img));

Yalnızca resimleri ertelemek istiyorsanız tarayıcının yerleşik tembel yükleme ipucu da vardır: <img> elemanının loading özniteliği ve onu yansıtan HTMLImageElement.loading özelliği "lazy" değerini alabilir. IntersectionObserver ise resim dışındaki işler için de kullanılabildiği, eşik ve pay üzerinde tam kontrol verdiği için daha geneldir: bir bileşeni ancak göründüğünde başlatmak, bir videoyu görünür olunca oynatmak gibi.

Desen 2: sonsuz kaydırma

Sonsuz kaydırmada listenin sonuna boş bir "nöbetçi" eleman koyar ve onu izlersiniz. Nöbetçi göründüğünde yeni öğeler getirilir. MDN'nin arayüz sayfasındaki örnek tam olarak bu fikri gösterir: oran sıfır veya altındaysa hiçbir şey yapılmaz, değilse yeni öğeler yüklenir.

html
<ul id="liste"></ul>
<div id="nobetci" aria-hidden="true"></div>
javascript
const nobetci = document.querySelector("#nobetci");
let yukleniyor = false;

const sonsuzObserver = new IntersectionObserver(async (entries) => {
  if (!entries[0].isIntersecting || yukleniyor) return;
  yukleniyor = true;
  try {
    await sonrakiSayfayiGetir(); // kendi fetch fonksiyonunuz
  } finally {
    yukleniyor = false;
  }
}, { rootMargin: "0px 0px 400px 0px" });

sonsuzObserver.observe(nobetci);

yukleniyor bayrağı aynı anda iki istek gitmesini engeller. Yeni öğeleri ekleme tarafı eleman oluşturma ve append yazısında, veriyi sunucudan çekme kısmı Fetch API kullanımı yazısında, async/await söz dizimi ise async/await rehberinde anlatılıyor. Bütün sayfalar bittiğinde sonsuzObserver.disconnect() çağırarak gözlemciyi tamamen durdurun.

Gözlemi durdurmak: unobserve, disconnect, takeRecords

Gözlemi durdurmak: unobserve, disconnect, takeRecords: unobserve(eleman), disconnect(), takeRecords()
  • unobserve(eleman): tek bir hedefin izlenmesini bırakır. Eleman zaten izlenmiyorsa hiçbir şey yapmaz, hata da fırlatmaz.
  • disconnect(): gözlemcinin bütün hedeflerini bırakır.
  • takeRecords(): henüz geri çağırmaya teslim edilmemiş girdileri dizi olarak döndürür ve bekleyen listeyi temizler; bu yüzden o girdiler için geri çağırma artık çalışmaz. Geri çağırmayla çalışıyorsanız buna genelde ihtiyacınız olmaz.

Aynı elemanı iki kez observe() ile vermek sorun çıkarmaz; MDN'ye göre zaten izlenen bir eleman için bu çağrı etkisizdir.

Geri çağırma ana iş parçacığında çalışır

API kesişim hesabını tarayıcıya bıraksa da sizin geri çağırmanız ana iş parçacığında çalışır. MDN, fonksiyonun olabildiğince hızlı olmasını, zaman alan işler için Window.requestIdleCallback() kullanılmasını önerir. Görünürlüğe bağlı görsel güncellemeleri bir sonraki kareye ertelemek için requestAnimationFrame de kullanılabilir; MDN'nin reklam görünürlük süresi örneği tam olarak böyle yapar.

Aynı örnek önemli bir sınırı da gösterir: sekme arka plana alındığında kesişim değişmez, çünkü kesişim sayfa görünürlüğünden etkilenmez. Bir elemanın kullanıcı tarafından gerçekten ne kadar süre görüldüğünü ölçüyorsanız visibilitychange olayını da dinleyip sayaçları duraklatmanız gerekir. Bu olay sayfa yaşam döngüsü yazısında anlatılıyor.

trackVisibility ve delay

Varsayılan gözlemci yalnızca hedefin kök alanına kaydırılıp kaydırılmadığına bakar. Hedefin üstü başka bir elemanla örtülmüş, opaklığı düşürülmüş ya da bir filtre veya dönüşümle bozulmuş olabilir; reklam gösterimi ölçümü gibi durumlarda bu fark önemlidir. trackVisibility: true verildiğinde tarayıcı hedefin görsel olarak bozulmadığını da kontrol eder. MDN bu hesabın pahalı olduğunu ve algoritmanın temkinli çalıştığını, hafif opaklık düşüşü olan elemanları bile görünür saymayabileceğini belirtir. Bu ayarla birlikte delay de verilmelidir: bildirimler arasındaki en kısa süreyi milisaniye cinsinden belirler ve görünürlük takibinde 100'den küçük değerler 100'e yükseltilir. Tarayıcı desteği için MDN uyumluluk tablosuna bakın.

Sık yapılan hatalar

  • İlk çağrıyı kullanıcı eylemi sanmak. observe() sonrasındaki ilk çağrı her zaman gelir; isIntersecting kontrolü yapın.
  • Ayarda thresholds yazmak. Doğru alan adı threshold; yanlış yazım eşiği sessizce [0] bırakır.
  • Kaydırma kutusu içinde root vermemek. Liste kendi kabında kayıyorsa kök olarak o kabı verin, hedef de onun soyundan olsun.
  • rootMargin içinde em veya rem kullanmak. Yalnızca px ve % geçerlidir; diğerleri SyntaxError ile sonuçlanır.
  • Tek seferlik işten sonra izlemeye devam etmek. Resim yüklendikten sonra unobserve() çağırmazsanız geri çağırma gereksiz yere çalışmayı sürdürür.
  • Geri çağırmada ağır iş yapmak. Fonksiyon ana iş parçacığında çalışır; uzun hesapları erteleyin.

Boyut değişimini izlemek için ResizeObserver ve MutationObserver yazısına bakın; üç gözlemci aynı observe / unobserve / disconnect düzenini kullanır ama farklı soruları cevaplar.

Sık Sorulan Sorular

IntersectionObserver CSS display: none olan elemanda çalışır mı?

Gözlemci dikdörtgenlerle çalışır; MDN, kesişim dikdörtgeni sıfır alanlı olduğunda veya hedef ile kök yalnızca bir kenarı paylaştığında bunun kesişim sayılmadığını söyler. Kutusu olmayan bir eleman için ilk çağrı yine gelir, ama isIntersecting: true içeren bir geçiş beklemeyin; elemanı izlemeden önce düzende yer kapladığından emin olun.

Kaydırma yönünü nasıl anlarım?

Girdi yalnızca o anki durumu verir. Yönü bulmak için önceki intersectionRatio veya boundingClientRect değerini bir değişkende saklayıp yenisiyle karşılaştırırsınız. MDN'nin örneği önceki oranı prevRatio değişkeninde tutarak oranın arttığını ya da azaldığını bulur.

iframe içindeki elemanlar izlenebilir mi?

MDN'nin kesişim hesabı açıklamasına göre hedef iç içe bir gezinme bağlamında (örneğin bir <iframe> belgesinde) ise kesişim dikdörtgeni önce o çerçevenin görüş alanına göre kırpılır, sonra hesap üst belgeye doğru devam eder. Yani çerçevenin kendisi kırpılmışsa içindeki hedef de buna göre daha az görünür sayılır.

Bir gözlemciye kaç eleman verebilirim?

Belgelerde bir sınır yazmaz; tek gözlemcinin birden çok hedefi izleyebilmesi API'nin tasarımının bir parçasıdır ve aynı ayarları paylaşan hedefler için tek gözlemci kullanmak, her eleman için ayrı gözlemci oluşturmaktan daha düzenlidir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · ResizeObserver ve MutationObserver · requestAnimationFrame · Sayfa yaşam döngüsü olayları · Fetch API kullanımı · Core Web Vitals