W3docs

JavaScript Intersection Observer API

Lerne die JavaScript Intersection Observer API kennen, um effizient zu erkennen, wann ein Element den viewport betritt oder verlässt — für Lazy Loading, Infinite Scroll und scrollbasierte Animationen.

Die Intersection Observer API ermöglicht es dir, den Browser zu bitten, dich zu benachrichtigen, wenn ein Element den sichtbaren Bereich der Seite betritt oder verlässt. Dies geschieht effizient und asynchron, ohne den Performanceaufwand, Scroll-Events selbst zu überwachen. Sie ist das richtige Werkzeug für Lazy-Loading von Bildern, den Aufbau von Infinite Scroll, das Auslösen von Animationen beim Erscheinen von Inhalten und die Messung, ob eine Anzeige oder ein Banner tatsächlich gesehen wurde.

Das Problem, das sie löst

Bevor diese API existierte, war die Beantwortung der einfachen Frage „Ist dieses Element gerade auf dem Bildschirm?" überraschend aufwendig. Man musste einen Listener an das scroll-Event (und oft auch an resize) anhängen und dann getBoundingClientRect() für jedes beobachtete Element aufrufen, um seine Position mit dem viewport zu vergleichen.

// The old, expensive way — runs on every scroll tick.
window.addEventListener('scroll', () => {
  const rect = element.getBoundingClientRect();
  const inView = rect.top < window.innerHeight && rect.bottom > 0;
  if (inView) {
    // do something
  }
});

Scroll-Events werden dutzende Male pro Sekunde ausgelöst, und getBoundingClientRect() zwingt den Browser zur Neuberechnung des Layouts (ein „Reflow"). Diese Arbeit synchron im Main Thread während eines Scrollvorgangs auszuführen ist eine klassische Ursache für Ruckeln (Jank). (Siehe Event Handling im DOM und JavaScript Scrolling für das Verhalten dieser Events.)

IntersectionObserver dreht das Modell um. Statt selbst Positionen abzufragen, beobachtet der Browser die Elemente für dich und ruft nur zurück, wenn sich die Sichtbarkeit tatsächlich ändert. Die Arbeit findet außerhalb des Main Threads statt, sodass das Scrollen nicht blockiert wird. Mehr dazu, warum das wichtig ist, findest du unter DOM Performance Optimization. Es ist ein enger Verwandter der MutationObserver API, die die DOM-Struktur auf Änderungen überwacht — nicht die Sichtbarkeit.

Grundlegende Verwendung

Du erstellst einen Observer mit einem Callback und teilst ihm dann über observe() mit, welche Elemente er beobachten soll.

// 1. Create an observer with a callback and (optional) options.
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Element is now visible:', entry.target);
    } else {
      console.log('Element left the viewport:', entry.target);
    }
  });
});

// 2. Start watching a target element.
const target = document.querySelector('#box');
observer.observe(target);

Der Callback erhält ein array von Einträgen, einen pro beobachtetem Element, dessen Sichtbarkeit sich geändert hat. Ein einzelner Observer kann viele Elemente beobachten, und das ist das empfohlene Muster — erstelle einen Observer und rufe observe() für jedes Ziel auf, anstatt für jedes Element einen eigenen Observer zu erstellen.

Info

Der Callback wird asynchron ausgeführt und Änderungen werden gebündelt — der Browser kann mehrere Einträge in einem Aufruf melden. Er wird auch einmal direkt nach dem Start der Beobachtung ausgelöst, sodass du den initialen Sichtbarkeitszustand des Elements erhältst, ohne auf ein Scroll-Event warten zu müssen. Die Browser-Unterstützung ist in allen modernen Browsern ausgezeichnet.

Den Observer konfigurieren

Das zweite Argument des Konstruktors ist ein Options-object mit drei Eigenschaften.

root

Das Element, das als viewport für die Sichtbarkeitsprüfung verwendet wird. Das Ziel muss ein Nachfahre des root sein. Wenn root den Wert null hat (Standard), wird der eigene viewport des Browsers verwendet.

const observer = new IntersectionObserver(callback, {
  root: document.querySelector('#scroll-container'),
});

rootMargin

Ein Abstand um den root, der wie ein CSS-margin-Wert geschrieben wird. Er vergrößert oder verkleinert die Box, die für Schnittmengen-Prüfungen verwendet wird. Ein üblicher Trick ist ein positiver unterer Margin, damit Elemente als „sichtbar" gemeldet werden, bevor sie tatsächlich in den viewport gescrollt werden — nützlich, um Inhalte frühzeitig zu laden.

const observer = new IntersectionObserver(callback, {
  // Trigger 200px before the element reaches the bottom edge.
  rootMargin: '0px 0px 200px 0px',
});

threshold

Eine Zahl von 0 bis 1 oder ein array von Zahlen, das dem Observer mitteilt, bei welchen Sichtbarkeitsverhältnissen er auslösen soll. 0 bedeutet „auslösen, sobald ein einzelnes Pixel sichtbar ist", 1 bedeutet „nur auslösen, wenn das Element vollständig sichtbar ist". Ein array löst bei jedem angegebenen Verhältnis aus.

const observer = new IntersectionObserver(callback, {
  // Fire at 0%, 50%, and 100% visibility.
  threshold: [0, 0.5, 1],
});

Der Inhalt eines Eintrags

Jedes object im entries-array beschreibt die Sichtbarkeit eines Elements zum Zeitpunkt des Callback-Aufrufs. Die nützlichsten Eigenschaften sind:

  • isIntersecting — ein boolean: true, wenn das Element aktuell innerhalb des root sichtbar ist.
  • intersectionRatio — wie viel des Elements sichtbar ist, von 0 bis 1.
  • target — das beobachtete Element.
  • boundingClientRect — Größe und Position des Ziels.
  • intersectionRect — der sichtbare Teil des Ziels.
  • rootBounds — das Rechteck des root (angepasst durch rootMargin).
  • time — ein Zeitstempel, wann die Änderung erfasst wurde.
const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.target.id, 'visible:', entry.isIntersecting);
    console.log('ratio:', entry.intersectionRatio.toFixed(2));
  }
});

Methoden: observe, unobserve, disconnect

Eine Observer-Instanz bietet dir drei Methoden:

  • observe(element) — beginne, ein Element zu beobachten.
  • unobserve(element) — höre auf, ein einzelnes Element zu beobachten.
  • disconnect() — höre auf, alle Elemente auf einmal zu beobachten.

Eine wichtige Best Practice: Sobald ein Element seine einmalige Aufgabe erledigt hat — beispielsweise ein Bild, das fertig lazy-geladen wurde — rufe unobserve() darauf auf, damit der Browser aufhört, etwas zu verfolgen, das sich nicht mehr ändern wird.

Anwendungsfall 1 — Lazy Loading von Bildern

Lazy Loading verschiebt das Herunterladen von Bildern, bis sie in Kürze sichtbar werden. Lege die echte URL in ein data-src-Attribut, beobachte jedes Bild und tausche sie in src aus, wenn es sichtbar wird — dann beende die Beobachtung.

<img data-src="photo-1.jpg" alt="First photo" width="600" height="400" />
<img data-src="photo-2.jpg" alt="Second photo" width="600" height="400" />
<img data-src="photo-3.jpg" alt="Third photo" width="600" height="400" />
const images = document.querySelectorAll('img[data-src]');

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;

    const img = entry.target;
    img.src = img.dataset.src;        // load the real image
    img.removeAttribute('data-src');
    observer.unobserve(img);          // job done — stop watching it
  });
}, { rootMargin: '0px 0px 200px 0px' }); // start loading a little early

images.forEach((img) => imageObserver.observe(img));
Hinweis

Moderne Browser unterstützen auch das native loading="lazy"-Attribut bei <img> und <iframe>, das überhaupt kein JavaScript benötigt. Greife auf IntersectionObserver zurück, wenn du benutzerdefiniertes Verhalten brauchst — einen Platzhalter-Tausch, einen Einblendeffekt oder das Laden von Nicht-Bild-Inhalten.

Anwendungsfall 2 — Infinite Scroll

Für Infinite Scroll platziere ein leeres „Sentinel"-Element am Ende der Liste. Wenn dieses Sentinel in den viewport scrollt, lade die nächste Datenseite und füge sie hinzu. Da das Sentinel unten bleibt, löst derselbe Observer weiterhin aus, während der Benutzer weiterscrollt.

<ul id="list"></ul>
<div id="sentinel"></div>
const list = document.querySelector('#list');
const sentinel = document.querySelector('#sentinel');
let page = 1;
let loading = false;

async function loadMore() {
  if (loading) return;            // guard against overlapping loads
  loading = true;

  const res = await fetch('/api/items?page=' + page);
  const items = await res.json();

  items.forEach((item) => {
    const li = document.createElement('li');
    li.textContent = item.title;
    list.appendChild(li);
  });

  page += 1;
  loading = false;
}

const scrollObserver = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMore();
  }
});

scrollObserver.observe(sentinel);

Anwendungsfall 3 — Reveal-on-Scroll-Animationen

Ein beliebter Effekt ist das Ein- oder Einblenden von Elementen, wenn sie den viewport betreten. Halte die Animation in CSS und lass JavaScript im richtigen Moment eine Klasse hinzufügen.

.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}
const revealItems = document.querySelectorAll('.reveal');

const revealObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target); // animate only once
    }
  });
}, { threshold: 0.15 }); // fire when ~15% is showing

revealItems.forEach((el) => revealObserver.observe(el));

Anwendungsfall 4 — Impression-/Sichtbarkeitstracking

Analytics müssen oft wissen, ob Inhalte tatsächlich gesehen wurden, nicht nur im DOM vorhanden waren. Ein höherer threshold ermöglicht es, eine Impression nur dann zu erfassen, wenn ein bedeutender Teil eines Elements sichtbar ist.

const adObserver = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.intersectionRatio >= 0.5) {
      sendImpression(entry.target.dataset.adId);
      adObserver.unobserve(entry.target); // count each ad once
    }
  });
}, { threshold: 0.5 }); // at least 50% visible

document.querySelectorAll('.ad').forEach((ad) => adObserver.observe(ad));

Dies könnte durch einen Timer erweitert werden, der beispielsweise eine volle Sekunde mit 50 % Sichtbarkeit erfordert, bevor eine Impression gezählt wird — ein gängiger Standard für „sichtbare" Anzeigen.

Zusammenfassung

Die Intersection Observer API ersetzt fragile, leistungshungrige Scroll-Listener durch eine saubere, asynchrone Möglichkeit, auf die Sichtbarkeit von Elementen zu reagieren. Erstelle einen Observer, weise ihn mit observe() auf deine Ziele hin, lies isIntersecting und intersectionRatio im Callback und rufe unobserve() auf, sobald die Aufgabe eines Elements erledigt ist. Mit root, rootMargin und threshold kannst du genau einstellen, wann er auslöst — sodass Lazy Loading, Infinite Scroll, Scroll-Animationen und Impression-Tracking sowohl einfach als auch reibungslos funktionieren.

Teste dein Wissen

Übung
Welche Eigenschaft eines Eintrags gibt an, ob das Ziel aktuell innerhalb des root sichtbar ist?
Welche Eigenschaft eines Eintrags gibt an, ob das Ziel aktuell innerhalb des root sichtbar ist?
Übung
Was ist der Hauptgrund, warum IntersectionObserver einem Scroll-Listener mit getBoundingClientRect() vorgezogen wird?
Was ist der Hauptgrund, warum IntersectionObserver einem Scroll-Listener mit getBoundingClientRect() vorgezogen wird?
Übung
Welche Aussagen über die Konstruktor-Optionen sind korrekt?
Welche Aussagen über die Konstruktor-Optionen sind korrekt?
Was this page helpful?