Intersection Observer

Lazy Loading Javascript Intersection Observer

Das Intersection Observer API beobachtet, ob (isIntersecting) und wie weit (Intersection Ratio) sich ein Element mit dem ViewPort überschneidet, um Animationen zu starten oder Dateien, Videos, iframes erst zu laden, wenn sie in den sichtbaren Sektor des ViewPorts kommen.

Scrollen und Rechnen: getBoundingClientRect

So startet die Animation auf dem Punkt, die Seite ist schneller aufgebaut und ein Datentransfer, der u.U. nicht benötigt wird, findet gar nicht erst statt.

Der Intersection Observer beobachtet Änderungen eines Elements im Verhältnis zu einem Eltern-Element – meist zum Viewport. Wie weit überschneidet sich das Element mit einem umgebenden Element? Die Informationen über die Änderungen wurden früher in erster Linie für das Nachladen von Inhalten beim Scrollen (z.B. lazy loading images) benutzt, bis das loading-Attribut für das img-Tag auf breiter Basis von allen Browsern unterstützt wurde.

Jetzt kümmert sich der Intersection Observer darum, dass CSS-Animation erst starten, wenn die Animation in den Viewport kommt – in den sichtbaren Ausschnitt im Browser.

Animierte Grafik Intersection Observer
Elemente und Animationen laden, wenn sie in den sichtbaren Bereich des Browserfensters kommen

Intersection Observer

Der Aufruf new IntersectionObserver hat zwei Argumente: Das erste ist eine Callback-Funktion, die ausgeführt wird, sobald ein Element in den Viewport kommt oder wenn sich der Abstand zwischen Elementen um einen gewissen Betrag geändert hat. Damit muss die Position eines Elements in Hinsicht auf ein anderes Element beim Scrollen nicht mehr permanent abgefragt werden.

Das zweite Argument listet die Optionen für den Intersection Observer.

               callback function  ──────┐
                                        |
                                        ▼
const io = new IntersectionObserver (handleEntries, options);
                                                       ▲
                                                       |
                                       Optionen  ──────┘

Intersection Observer Options

Mit den Default-Optionen des IntersectionObserver schaltet die Callback-Funktion, wenn ein Element teilweise in den Viewport kommt und den ViewPort verläßt. Das Observer API arbeitet nicht mit einem Pixelwert für das Überschneiden der Elemente, sondern reagiert, wenn die Überschneidung so irgendwie bei dem angegebenen Prozentsatz des threshold liegt.

root
Das Elternelement oder der Viewport, in dem das Element liegt. null steht für den Viewport des Browsers.
threshold
ist ein Wert oder ein Array von Werten zwischen 0 und 1, bei denen der Intersection Observer die Callback-Funktion ausführt.
rootMargin
Beim Umfang des Überscheidungsbereichs (root) wird ein Abstand eingerechnet, ähnlich dem Margin bei CSS.

threshold und rootMargin

threshold = 0.5 würde beim Betreten und beim Verlassen des Root-Elements die Callback-Funktion aufrufen. Das könnte z.B. der Ablauf für ein Video sein, das bei Erscheinen im Viewport anläuft und pausiert, wenn es den Viewport verläßt.

Ist threshold ein Array – z.B. threshold: [0, 0.25, 0.5, 0.75, 1] –, wird die Callback-Funktion bei jedem der Werte aufgerufen (scrollen innerhalb der gelben Scrollbox).

Intersection Observer rootMargin=0-threshold
rootMargin=0 / threshold=1
Das Element wird geladen, wenn es 100% im sichtbaren Ausschnitt ist
Intersection Observer rootMargin=0-threshold
rootMargin=50 / threshold=0
Das Element wird geladen, sobald es den Abstand rootMargin="50" betritt
Intersection Observer rootMargin=0-threshold
rootMargin=0 / threshold=0.5
Callback-Funktion aufrufen, wenn das Element zu 50% im sichtbaren Ausschnitt ist
Intersection Observer rootMargin=0-threshold
rootMargin=0 / threshold=0.5
Callback-Funktion aufrufen, wenn das Element den sichtbaren Ausschnitt zu 50% verlassen hat

Effekt: CSS transition-delay – Animation nach einer kurzen Verzögerung

CSS Animation starten

Mit der Feststellung, dass ~150px des Elements im Browserfenster sichtbar sind, startet eine Animation.

CSS Transition und Transform Scale beim Scrollen
HTML
<figure class="image-container">
	<div class="jade-scale"></div>
	<figcaption>CSS Transition und Transform Scale beim Scrollen</figcaption>
</figure>

CSS transition und transform scale

.jade {
	background: url('marienkaefer-720.webp') no-repeat;
	background-size: cover;
	background-position: center center;
	transition: transform 5s ease;
	transform: scale(1);
}

.jade-scale.image-scaling {
	transform: scale(1.8);
}

Und das Skript:

const jadeScale = document.querySelectorAll(".jade-scale");
const options = {rootMargin: "-150px"};

const jadeScaleObserver = new IntersectionObserver (function (entries, observer) {
	entries.forEach(function(entry) {
		if (entry.isIntersecting) {
			entry.target.classList.add("image-scaling");
		} else {
			entry.target.classList.remove("image-scaling");
		}
	});
}, options);

jadeScale.forEach ( function (jadeScale) {
	jadeScaleObserver.observe (jadeScale);
});

Die Option {threshold: 0.5} würde dafür sorgen, dass die Animation erst startet, wenn das Bild zu 50% im ViewPort ist.

Einfliegen von Links: Intersection Observer

Das Lazy Loading von Bildern, iframes und Video wird von Chrome, Edge, Firefox und Opera bereits durch ein einfaches HTML-Attribut eingesetzt: loading="lazy". Heute wird der Intersection Observer in erster Linie für das Starten von CSS-Animationen genutzt – z.B. das »fly in«, das Einfliegen eines Elements.

Wenn das Element mit der CSS-Klasse river in den Viewport kommt, setzt die Callback-Funktion eine zusätzliche Klasse swimming ein. Das Bild soll von links bis zur Mitte des blauen Bands einfliegen.

<div class="river">
	<div class="crossing">
		<img src="turtle-blue.svg" width="200" height="156" alt="turtle crossing">
	</div>
</div>
.swimming {
	animation: swimmer 10s ease-out forwards;
}

@keyframes swimmer { 
	from {transform: translateX(calc(-200px ))}
	to {transform: translateX(calc(50% - 100px))}
}
Schildkröte crossing
const river = document.querySelector (".river");
const crossing = document.querySelector (".crossing");

const turtleOptions = {
	root: null,
	rootMargin: "0px",
	threshold: 0.7,
};

const callback = function (entries, observer) {
	const observed = entries[0];
	if (observed.isIntersecting) {
		document.querySelector(".crossing").classList.add("swimming");
	}
}

const observer = new IntersectionObserver (callback, turtleOptions);

if (river) {
	observer.observe (river);
}

Das Skript ist ziemlich einfach, das CSS hingegen ist trickreich. Ein transformiertes Element kennt nur sein eigenes Koordinatensystem und transform(translate: 100%) bezieht sich nur auf die Breite des Elements. Darum animiert das Skript nicht das Bild, sondern seinen Container mit der CSS-Klasse river.

Mehr zu transform: translate()

Hintergrundbilder: Lazy Loading per Intersection Observer

Im einfachsten Fall – Bilder nachladen, wenn sie in den Viewport kommen –, braucht der Intersection Observer nur wenige Zeilen. Die Elemente der Slideshow haben eine zusätzliche CSS-Klasse lazy, das die CSS-Eigenschaft background-image: none setzt.

.lazy.slide.slide1,
.lazy.slide.slide2,
.lazy.slide.slide3,
.lazy.slide.slide4 { background-image: none}

.slide.slide1.inview {
	background: url(flowers--04.webp);
}
.slide.slide2.inview  {
	background: url(flowers--03.webp);
}
.slide.slide3.inview  {
	background: url(flowers--02.webp);
}
.slide.slide4.inview  {
	background: url(flowers--01.webp);
}

Wenn die Slideshow in den sichtbaren Ausschnitt des Browserfensters kommt, fügt das Script die Klasse inview hinzu, um die Hintergrundbilder zu laden.

Slide 1
Slide 2
Slide 3
Slide 4
Slide 1

Heute unterstützen alle modernen Browser das loading lazy-Attribut und der Intersection Observer wird für das Nachladen von Bildern nicht mehr gebraucht. Für Hintergrundbilder besteht diese einfache Möglichkeit nicht.

Der Platzhalter und die Angabe von width und height sind wichtig für ein stabiles Layout: Schwappt das Bild nachträglich in die Seite, käme es zu einem Cumulative Layout Shift (CLS) – das Layout verschiebt sich. Ärgerlich, wenn man gerade einen Text liest und noch ärgerlicher, wenn jetzt ein Klick zu einem falschen Link führt.

Wenn mehrere Elemente beobachtet werden sollen, sollten nach Möglichkeit alle Elemente vom selben IntersectionObserver durch mehrfache Aufrufe von observer () überwacht werden.

Script

const lazyBg = document.querySelectorAll(".slide");

const lazyBackgroundObserver = new IntersectionObserver (function (entries, observer) {
	entries.forEach(function(entry) {
		if (entry.isIntersecting) {
			entry.target.classList.add("inview");
		}
	},{});
});

lazyBg.forEach ( function (lazyBackground) {
	lazyBackgroundObserver.observe(lazyBackground);
});

Die Optionen in diesem Beispiel sind leer: Per Voreinstellung gilt der Viewport als Elternelement. Das Bild wird angezeigt, wenn es im Viewport sichtbar ist.

Bilder, die over the fold beim Laden der Seite angezeigt werden, sollten natürlich nicht per Observer geladen werden.

Wenn Bilder responsive mit HTML srcset eingebunden werden, kann die Callback-Funktion im IntersectionObserver die matchMedia-Abfrage einsetzen.

unobserve

Wenn das observierte Element nicht länger beobachtet werden soll – z.B. die Animation nach Ablauf nicht erneut starten soll oder Dateien nicht erneut geladen werden sollen, beendet die Methode unobserve den Intersection Observer.

if (entry.isIntersecting) {
	loadFile (entry)
	observer.unobserve(entry.target)
}

Informationen des Intersection Observer

Zu den schönen Seite des Web Animation API zählt, dass die Browserkonsole und Debugging Einblick in den Ablauf der Animation geben.

Ein Blick in die Konsole zeigt gleich beim Laden der Seite, dass sich das Element noch nicht mit dem root-Element überschneidet (intersectionRatio: 0) und isIntersecting steht noch auf false.

boundingClientRect: DOMRectReadOnly {x: 369, y: 55.0625, width: 900, height: 622, top: 55.0625, …}
intersectionRatio: 0
intersectionRect: DOMRectReadOnly {x: 369, y: 55.0625, width: 900, height: 621.9375, top: 55.0625, …}
isIntersecting: false
rootBounds: DOMRectReadOnly {x: 0, y: 0, width: 2019, height: 677, top: 0, …}
target: <div class="simple">
time: 22273

Sobald das Element in den sichtbaren Ausschnitt des Browserfensters kommt, ist intersectionRatio auf 1 gesetzt und isIntersecting true.

boundingClientRect: DOMRectReadOnly {x: 369, y: 51.0625, width: 900, height: 622, top: 51.0625, …}
intersectionRatio: 1
intersectionRect: DOMRectReadOnly {x: 369, y: 51.0625, width: 900, height: 622, top: 51.0625, …}
isIntersecting: true
rootBounds: DOMRectReadOnly {x: 0, y: 0, width: 2019, height: 677, top: 0, …}
target: <div class="simple">
time: 31279

Relativ und fixed positionierte Intersection-Roots

Wenn Target-Elemente absolut innerhalb eines relativ positionierten Elements liegen, meldet der Intersection Observer keinen Treffer, wenn das Target-Element den relativ positionierten Root überschneidet.

Das gilt wohl auch für Target-Element mit position:fixed (?).

Browser-Support für IE

Zwar sind Browser ohne Javascript zu einer Seltenheit geworden, aber wenn sie dennoch beachtet werden sollen, kann z.B. ein noscript-Tag eingesetzt werden.

Die Abfrage, ob der IntersectionObserver im Window-Objekt vorhanden ist,

if (IntersectionObserver in window) {
	…	…	…
}

hilft älteren Browsern, insbesondere IE, der das Intersection Observer-API nicht unterstützt. Falls nicht, lädt das Script die Merkmale direkt.

resizeObserver

Neben dem intersectionObserver gibt es ein weiteres Application Interface – resizeObserver, das genauso wie der Intersection Observer durch eine Callback-Funktion initialisiert wird.

Das ResizeObserver-Objekt beschränkt die Beobachtung auf einzelne Elemente, während das globale window.resize bei jeder Größenänderung des Viewports feuert und schnell zu Leistungseinbußen führt.