Jetzt sehen Sie mich: Wie man mit IntersectionObserver aufschiebt, faul lädt und handelt
Veröffentlicht: 2022-03-10Es war einmal ein Webentwickler, der seine Kunden erfolgreich davon überzeugte, dass Websites nicht in allen Browsern gleich aussehen sollten, sich um Barrierefreiheit kümmerte und ein früher Anwender von CSS-Grids war. Aber tief in seinem Herzen war Performance seine wahre Leidenschaft: Er optimierte, minimierte, überwachte und setzte sogar psychologische Tricks in seinen Projekten ein.
Eines Tages erfuhr er dann von Lazy-Loading-Bildern und anderen Assets, die für Benutzer nicht sofort sichtbar und für die Wiedergabe sinnvoller Inhalte auf dem Bildschirm nicht unbedingt erforderlich sind. Es war der Anfang der Morgendämmerung: Der Entwickler betrat die böse Welt der Lazy-Loading-jQuery-Plugins (oder vielleicht die nicht so böse Welt der async
und defer
-Attribute). Einige sagen sogar, dass er direkt in den Kern aller Übel geraten ist: die Welt der scroll
-Event-Listener. Wir werden nie genau wissen, wo er gelandet ist, aber andererseits ist dieser Entwickler absolut fiktiv, und jede Ähnlichkeit mit irgendeinem Entwickler ist rein zufällig.
Nun, Sie können jetzt sagen, dass die Büchse der Pandora geöffnet wurde und dass unser fiktiver Entwickler das Problem nicht weniger real macht. Heutzutage ist die Priorisierung von Inhalten "above the fold" für die Leistung unserer Webprojekte sowohl im Hinblick auf die Geschwindigkeit als auch auf das Seitengewicht äußerst wichtig.
In diesem Artikel verlassen wir die scroll
Dunkelheit und sprechen über die moderne Art des verzögerten Ladens von Ressourcen. Nicht nur Lazy-Loading von Bildern, sondern auch das Laden von Assets. Darüber hinaus kann die Technik, über die wir heute sprechen werden, viel mehr als nur Lazy-Loading von Assets: Wir werden in der Lage sein, jede Art von verzögerter Funktionalität basierend auf der Sichtbarkeit der Elemente für Benutzer bereitzustellen.
Meine Damen und Herren, lassen Sie uns über die Intersection Observer API sprechen. Aber bevor wir beginnen, werfen wir einen Blick auf die Landschaft der modernen Tools, die uns zu IntersectionObserver
geführt hat.
2017 war ein sehr gutes Jahr für in unsere Browser integrierte Tools, die uns dabei geholfen haben, die Qualität sowie den Stil unserer Codebasis ohne allzu großen Aufwand zu verbessern. Heutzutage scheint sich das Web weg von sporadischen Lösungen zu bewegen, die auf sehr unterschiedlichen Lösungen basieren, hin zu einem klar definierten Ansatz von Observer-Schnittstellen (oder einfach nur „Observers“): MutationObserver, der gut unterstützt wird, hat schnell neue Familienmitglieder bekommen in modernen Browsern übernommen:
- IntersectionObserver und
- PerformanceObserver (als Teil der Performance Timeline Level 2-Spezifikation).
Ein weiteres potenzielles Familienmitglied, FetchObserver, ist in Arbeit und führt uns mehr in die Länder eines Netzwerk-Proxys, aber heute möchte ich stattdessen mehr über Front-End sprechen.
PerformanceObserver
und IntersectionObserver
zielen darauf ab, Frontend-Entwicklern dabei zu helfen, die Leistung ihrer Projekte an verschiedenen Punkten zu verbessern. Ersteres gibt uns das Werkzeug für das Real User Monitoring, während letzteres das Werkzeug ist, das uns eine spürbare Leistungssteigerung bietet. Wie bereits erwähnt, befasst sich dieser Artikel genau mit letzterem: IntersectionObserver . Um insbesondere die Mechanik von IntersectionObserver
zu verstehen, sollten wir einen Blick darauf werfen, wie ein generischer Observer im modernen Web funktionieren soll.
Profi-Tipp : Sie können die Theorie überspringen und direkt in die Mechanik von IntersectionObserver eintauchen oder, noch weiter, direkt zu den möglichen Anwendungen von IntersectionObserver
.
Beobachter vs. Ereignis
Ein „Observer“, wie der Name schon sagt, soll etwas beobachten, was im Kontext einer Seite passiert. Beobachter können beobachten, was auf einer Seite passiert, wie DOM-Änderungen. Sie können auch nach Lebenszyklusereignissen der Seite Ausschau halten. Beobachter können auch einige Callback-Funktionen ausführen. Nun könnte ein aufmerksamer Leser das Problem hier sofort erkennen und fragen: „Also, was ist der Punkt? Haben wir dazu nicht schon Veranstaltungen? Was unterscheidet Beobachter?“ Sehr guter Punkt! Schauen wir uns das genauer an und sortieren es aus.
Der entscheidende Unterschied zwischen regulärem Event und Observer besteht darin, dass Ersteres standardmäßig auf jedes Auftreten des Events synchron reagiert, was die Reaktionsfähigkeit des Haupt-Threads beeinflusst, während Letzteres asynchron reagieren sollte, ohne die Leistung so stark zu beeinträchtigen. Zumindest gilt das für die aktuell vorgestellten Observer: Alle verhalten sich asynchron , und ich glaube nicht, dass sich das auch in Zukunft ändern wird.
Dies führt zu dem Hauptunterschied im Umgang mit den Callbacks von Observers, der Anfänger verwirren könnte: Die asynchrone Natur von Observers kann dazu führen, dass mehrere Observables gleichzeitig an eine Callback-Funktion übergeben werden. Aus diesem Grund sollte die Callback-Funktion keinen einzelnen Eintrag erwarten, sondern ein Array
von Einträgen (auch wenn das Array manchmal nur einen Eintrag enthält).
Darüber hinaus bieten einige Beobachter (insbesondere der, über den wir heute sprechen) sehr praktische vorberechnete Eigenschaften, die wir sonst mit (vom Leistungsstandpunkt) teuren Methoden und Eigenschaften bei der Verwendung regulärer Ereignisse selbst berechnet haben. Um diesen Punkt zu verdeutlichen, kommen wir etwas später in diesem Artikel zu einem Beispiel.
Wenn es also für jemanden schwierig ist, vom Event-Paradigma wegzukommen, würde ich sagen, dass Observer Events auf Steroiden sind. Eine andere Beschreibung wäre: Beobachter sind eine neue Ebene der Annäherung an die Geschehnisse. Aber egal, welche Definition Sie bevorzugen, es sollte selbstverständlich sein, dass Beobachter (zumindest noch nicht) Ereignisse ersetzen sollen; es gibt genug Anwendungsfälle für beide, und sie können glücklich nebeneinander leben.
Struktur des generischen Beobachters
Die generische Struktur eines Beobachters (alle zum Zeitpunkt des Schreibens verfügbaren) sieht ähnlich aus wie folgt:
/** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);
Beachten Sie auch hier, dass es sich bei entries
um ein Array
von Werten handelt, nicht um einen einzelnen Eintrag.
Dies ist die generische Struktur: Implementierungen bestimmter Observer unterscheiden sich in den Argumenten, die an ihre observe()
übergeben werden, und den Argumenten, die an ihren Callback übergeben werden. Beispielsweise sollte MutationObserver
auch ein Konfigurationsobjekt erhalten, um mehr darüber zu erfahren, welche Änderungen im DOM zu beobachten sind. PerformanceObserver
beobachtet keine Knoten im DOM, sondern verfügt stattdessen über den dedizierten Satz von Eintragstypen, den es beobachten kann.
Lassen Sie uns hier den „allgemeinen“ Teil dieser Diskussion beenden und tiefer in das Thema des heutigen Artikels eintauchen – IntersectionObserver
.
Dekonstruieren von IntersectionObserver
Lassen Sie uns zunächst klären, was IntersectionObserver
ist.
Laut MDN:
Die Schnittmengenbeobachter-API bietet eine Möglichkeit, Änderungen an der Schnittmenge eines Zielelements mit einem übergeordneten Element oder mit dem Ansichtsfenster eines Dokuments der obersten Ebene asynchron zu beobachten.
Einfach ausgedrückt: IntersectionObserver
beobachtet asynchron die Überlappung eines Elements mit einem anderen Element. Lassen Sie uns darüber sprechen, wozu diese Elemente in IntersectionObserver
dienen.
IntersectionObserver-Initialisierung
In einem der vorherigen Abschnitte haben wir die Struktur eines generischen Beobachters gesehen. IntersectionObserver
erweitert diese Struktur ein wenig. Zunächst erfordert diese Art von Observer eine Konfiguration mit drei Hauptelementen:
-
root
: Dies ist das Wurzelelement, das für die Beobachtung verwendet wird. Es definiert den grundlegenden „Erfassungsrahmen“ für beobachtbare Elemente. Standardmäßig ist derroot
der Ansichtsbereich Ihres Browsers, kann aber wirklich jedes Element in Ihrem DOM sein (dann setzen Sie denroot
auf etwas wiedocument.getElementById('your-element')
). Denken Sie jedoch daran, dass die Elemente, die Sie beobachten möchten, in diesem Fall im DOM-Baum vonroot
„leben“ müssen.
-
rootMargin
: Definiert den Rand um Ihrroot
, der den „Erfassungsrahmen“ erweitert oder verkleinert , wenn die Abmessungen Ihrerroot
nicht genügend Flexibilität bieten. Die Optionen für die Werte dieser Konfiguration ähneln denen vonmargin
in CSS, z. B.rootMargin: '50px 20px 10px 40px'
(top, right bottom, left). Die Werte können abgekürzt werden (wierootMargin: '50px'
) und entweder inpx
oder%
ausgedrückt werden. Standardmäßig istrootMargin: '0px'
.
-
threshold
: Es ist nicht immer erwünscht, sofort zu reagieren, wenn ein beobachtetes Element eine Grenze des „Erfassungsrahmens“ schneidet (definiert als eine Kombination ausroot
undrootMargin
).threshold
definiert den Prozentsatz eines solchen Schnittpunkts, bei dem Observer reagieren soll. Er kann als einzelner Wert oder als Array von Werten definiert werden. Um den Effekt desthreshold
besser zu verstehen (ich weiß, dass es manchmal verwirrend sein kann), sind hier einige Beispiele:-
threshold: 0
: Der StandardwertIntersectionObserver
soll reagieren, wenn das allererste oder allerletzte Pixel eines beobachteten Elements eine der Grenzen des „Erfassungsrahmens“ schneidet. Denken Sie daran, dassIntersectionObserver
richtungsunabhängig ist, was bedeutet, dass er in beiden Szenarien reagiert: a) wenn das Element eintritt und b) wenn es den „Erfassungsrahmen“ verlässt . -
threshold: 0.5
: Beobachter sollte ausgelöst werden, wenn 50 % eines beobachteten Elements den „Erfassungsrahmen“ schneidet; -
threshold: [0, 0.2, 0.5, 1]
: Beobachter soll in 4 Fällen reagieren:- Das allererste Pixel eines beobachteten Elements tritt in den „Erfassungsrahmen“ ein: das Element befindet sich immer noch nicht wirklich innerhalb dieses Rahmens, oder das allerletzte Pixel des beobachteten Elements verlässt den „Erfassungsrahmen“: das Element befindet sich nicht mehr innerhalb des Rahmens;
- 20 % des Elements befinden sich innerhalb des „Erfassungsrahmens“ (auch hier spielt die Richtung für
IntersectionObserver
keine Rolle); - 50 % des Elements befinden sich innerhalb des „Erfassungsrahmens“;
- 100 % des Elements befinden sich innerhalb des „Erfassungsrahmens“. Dies ist genau das Gegenteil von
threshold: 0
.
-
Um unseren IntersectionObserver
über unsere gewünschte Konfiguration zu informieren, übergeben wir einfach unser Konfigurationsobjekt zusammen mit unserer config
-Funktion wie folgt an den Konstruktor unseres Observers:
const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);
Jetzt sollten wir IntersectionObserver
das eigentliche zu beobachtende Element geben. Dies geschieht einfach, indem das Element an die Funktion observe()
übergeben wird:
… const img = document.getElementById('image-to-observe'); observer.observe(image);
Zu diesem beobachteten Element sind einige Dinge zu beachten:
- Es wurde bereits erwähnt, aber es lohnt sich, es noch einmal zu erwähnen: Wenn Sie
root
als Element im DOM festlegen, sollte sich das beobachtete Element im DOM-Baum vonroot
befinden. -
IntersectionObserver
kann jeweils nur ein Element zur Beobachtung akzeptieren und unterstützt keine Stapelbereitstellung für Beobachtungen. Das heißt, wenn Sie mehrere Elemente beobachten müssen (z. B. mehrere Bilder auf einer Seite), müssen Sie über alle iterieren und jedes einzeln beobachten:
… const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
- Wenn Sie eine Seite mit Observer laden, stellen Sie möglicherweise fest, dass der Callback von
IntersectionObserver
für alle beobachteten Elemente gleichzeitig ausgelöst wurde. Auch solche, die nicht zur mitgelieferten Konfiguration passen. „Nun … nicht wirklich das, was ich erwartet hatte“, ist der übliche Gedanke, wenn man das zum ersten Mal erlebt. Aber lassen Sie sich hier nicht verwirren: Das bedeutet nicht notwendigerweise, dass diese beobachteten Elemente den „Erfassungsrahmen“ irgendwie schneiden, während die Seite geladen wird.
Das bedeutet jedoch, dass der Eintrag für dieses Element initialisiert wurde und nun von Ihrem IntersectionObserver
gesteuert wird. Dies kann Ihrer Callback-Funktion jedoch unnötiges Rauschen hinzufügen, und es liegt in Ihrer Verantwortung, zu erkennen, welche Elemente tatsächlich den „Erfassungsrahmen“ schneiden und welche wir immer noch nicht berücksichtigen müssen. Um zu verstehen, wie diese Erkennung durchgeführt wird, gehen wir etwas tiefer in die Anatomie unserer Rückruffunktion ein und werfen einen Blick darauf, woraus solche Einträge bestehen.
IntersectionObserver-Callback
Zunächst einmal nimmt die Callback-Funktion für einen IntersectionObserver
zwei Argumente entgegen, und wir werden diese in umgekehrter Reihenfolge besprechen, beginnend mit dem zweiten Argument. Zusammen mit dem oben erwähnten Array
beobachteter Einträge, das unseren „Erfassungsrahmen“ schneidet, erhält die Rückruffunktion den Beobachter selbst als zweites Argument.
Verweis auf den Beobachter selbst
new IntersectionObserver(function(entries, SELF) {…});
Das Abrufen des Verweises auf den Observer selbst ist in vielen Szenarien nützlich, wenn Sie die Beobachtung eines Elements beenden möchten, nachdem es zum ersten Mal vom IntersectionObserver
erkannt wurde. Szenarien wie verzögertes Laden der Bilder, verzögertes Abrufen anderer Assets usw. gehören zu dieser Art. Wenn Sie die Beobachtung eines Elements beenden möchten, stellt IntersectionObserver
eine unobserve(element-to-stop-observing)
-Methode bereit, die in der Callback-Funktion ausgeführt werden kann, nachdem Sie einige Aktionen für das beobachtete Element durchgeführt haben (wie z. B. das tatsächliche verzögerte Laden eines Bilds). ).
Einige dieser Szenarien werden in diesem Artikel weiter besprochen, aber nachdem wir dieses zweite Argument aus dem Weg geräumt haben, kommen wir zu den Hauptdarstellern dieses Callback-Spiels.
KreuzungBeobachterEintrag
new IntersectionObserver(function(ENTRIES, self) {…});
Die entries
, die wir in unserer Callback-Funktion als Array
erhalten, sind vom speziellen Typ: IntersectionObserverEntry
. Diese Schnittstelle stellt uns einen vordefinierten und vorberechneten Satz von Eigenschaften bezüglich jedes einzelnen beobachteten Elements zur Verfügung. Werfen wir einen Blick auf die interessantesten.
Zunächst enthalten Einträge vom Typ IntersectionObserverEntry
Informationen über drei verschiedene Rechtecke, die Koordinaten und Grenzen der am Prozess beteiligten Elemente definieren:
-
rootBounds
: Ein Rechteck für den „Erfassungsrahmen“ (root
+rootMargin
); -
boundingClientRect
: Ein Rechteck für das beobachtete Element selbst; -
intersectionRect
: Ein Bereich des „Erfassungsrahmens“, der von dem beobachteten Element geschnitten wird.
Das wirklich Coole daran, dass diese Rechtecke für uns asynchron berechnet werden, ist, dass sie uns wichtige Informationen zur Positionierung des Elements liefern, ohne dass wir getBoundingClientRect()
, offsetTop
, offsetLeft
und andere teure Positionierungseigenschaften und -methoden aufrufen, die ein Layout-Thrashing auslösen. Reiner Leistungsgewinn!
Eine weitere für uns interessante Eigenschaft der Schnittstelle IntersectionObserverEntry
ist isIntersecting
. Dies ist eine Bequemlichkeitseigenschaft, die angibt, ob das beobachtete Element gerade den „Erfassungsrahmen“ schneidet oder nicht. Wir könnten diese Informationen natürlich erhalten, indem wir uns das intersectionRect
(wenn dieses Rechteck nicht 0 × 0 ist, schneidet das Element den „Erfassungsrahmen“), aber es ist ziemlich bequem, dies für uns vorberechnet zu haben.
isIntersecting
lässt sich herausfinden, ob das beobachtete Element gerade in den „Capturing Frame“ eintritt oder diesen bereits verlässt. Um dies herauszufinden, speichern Sie den Wert dieser Eigenschaft als globales Flag, und wenn der neue Eintrag für dieses Element in Ihrer Callback-Funktion ankommt, vergleichen Sie seinen neuen isIntersecting
mit diesem globalen Flag:
- Wenn es
false
war und jetzttrue
ist, dann tritt das Element in den „Erfassungsrahmen“ ein; - Wenn es umgekehrt ist und es jetzt
false
ist, während es vorhertrue
war, dann verlässt das Element den „Erfassungsrahmen“.
isIntersecting
ist genau die Eigenschaft, die uns hilft, das zuvor besprochene Problem zu lösen, dh getrennte Einträge für die Elemente, die den „Erfassungsrahmen“ tatsächlich schneiden, von dem Rauschen derer, die nur die Initialisierung des Eintrags sind.
let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);
HINWEIS : In Microsoft Edge 15 wurde die Eigenschaft isIntersecting
nicht implementiert, was trotz vollständiger Unterstützung für IntersectionObserver
ansonsten undefined
zurückgibt. Dies wurde jedoch im Juli 2017 behoben und ist seit Edge 16 verfügbar.
Die Schnittstelle IntersectionObserverEntry
bietet eine weitere vorberechnete Komforteigenschaft: intersectionRatio
. Dieser Parameter kann für die gleichen Zwecke wie isIntersecting
verwendet werden, bietet jedoch eine genauere Steuerung, da es sich um eine Fließkommazahl anstelle eines booleschen Werts handelt. Der Wert von intersectionRatio
gibt an, wie viel des Bereichs des beobachteten Elements den „Erfassungsrahmen“ schneidet (das Verhältnis des Bereichs „ intersectionRect
“ zum Bereich „ boundingClientRect
“). Auch hier könnten wir diese Berechnung anhand der Informationen aus diesen Rechtecken selbst durchführen, aber es ist gut, dies für uns erledigen zu lassen.
target
ist eine weitere Eigenschaft der IntersectionObserverEntry
-Schnittstelle, auf die Sie möglicherweise häufig zugreifen müssen. Aber hier gibt es absolut keine Magie – es ist nur das ursprüngliche Element, das an die Funktion observe()
Ihres Beobachters übergeben wurde. Genau wie event.target
, an das Sie sich bei der Arbeit mit Ereignissen gewöhnt haben.
Um die vollständige Liste der Eigenschaften für die IntersectionObserverEntry
-Schnittstelle zu erhalten, überprüfen Sie die Spezifikation.
Mögliche Anwendungen
Mir ist klar, dass Sie höchstwahrscheinlich genau wegen dieses Kapitels zu diesem Artikel gekommen sind: Wen interessiert die Mechanik, wenn wir doch Codeschnipsel zum Kopieren und Einfügen haben? Wir wollen Sie jetzt also nicht mit weiteren Diskussionen belästigen: Wir begeben uns in das Land des Codes und der Beispiele. Ich hoffe, dass die im Code enthaltenen Kommentare die Dinge klarer machen.
Zurückgestellte Funktionalität
Sehen wir uns zunächst ein Beispiel an, das die grundlegenden Prinzipien aufzeigt, die der Idee von IntersectionObserver
zugrunde liegen. Nehmen wir an, Sie haben ein Element, das viele Berechnungen durchführen muss, sobald es auf dem Bildschirm angezeigt wird. Beispielsweise sollte Ihre Anzeige nur dann eine Ansicht registrieren, wenn sie tatsächlich einem Benutzer gezeigt wurde. Aber jetzt stellen wir uns vor, dass Sie irgendwo unterhalb des ersten Bildschirms auf Ihrer Seite ein automatisch abgespieltes Karussellelement haben.
Ein Karussell zu betreiben ist im Allgemeinen eine schwere Aufgabe. Normalerweise handelt es sich um JavaScript-Timer, Berechnungen zum automatischen Scrollen durch die Elemente usw. All diese Aufgaben laden den Hauptthread, und wenn dies im Autoplay-Modus erledigt ist, ist es für uns schwer zu wissen, wann unser Hauptthread diesen Treffer erhält. Wenn wir über die Priorisierung von Inhalten auf unserem ersten Bildschirm sprechen und so schnell wie möglich First Meaningful Paint und Time To Interactive erreichen wollen, wird ein blockierter Haupt-Thread zu einem Engpass für unsere Leistung.
Um das Problem zu beheben, können wir die Wiedergabe eines solchen Karussells verschieben, bis es in den Darstellungsbereich des Browsers gelangt. Für diesen Fall werden wir unser Wissen und unser Beispiel für den isIntersecting
Parameter der IntersectionObserverEntry
-Schnittstelle einsetzen.
const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);
Hier spielen wir das Karussell nur ab, wenn es in unser Ansichtsfenster gelangt. Beachten Sie das Fehlen eines config
, das an die Initialisierung von IntersectionObserver
übergeben wird: Dies bedeutet, dass wir uns auf die Standardkonfigurationsoptionen verlassen. Wenn das Karussell unser Ansichtsfenster verlässt, sollten wir aufhören, es zu spielen, um keine Ressourcen für die nicht mehr wichtigen Elemente auszugeben.
Faules Laden von Assets
Dies ist wahrscheinlich der offensichtlichste Anwendungsfall für IntersectionObserver
: Wir wollen keine Ressourcen ausgeben, um etwas herunterzuladen, das der Benutzer gerade nicht benötigt. Dies bietet Ihren Benutzern einen großen Vorteil: Benutzer müssen nichts herunterladen, und ihre mobilen Geräte müssen nicht viele nutzlose Informationen parsen und zusammenstellen, die sie im Moment nicht benötigen. Es überrascht nicht, dass es auch der Leistung Ihrer App zugute kommt.
Um das Herunterladen und Verarbeiten von Ressourcen zu verzögern, bis der Benutzer sie möglicherweise auf dem Bildschirm anzeigt, haben wir uns bisher mit Ereignis-Listenern für Ereignisse wie scroll
beschäftigt. Das Problem liegt auf der Hand: Das hat die Zuhörer viel zu oft getriggert. Also mussten wir auf die Idee kommen, die Ausführung des Callbacks zu drosseln oder zu entprellen. Aber all dies hat unseren Haupt-Thread stark unter Druck gesetzt und ihn blockiert, als wir ihn am dringendsten brauchten.
Zurück zu IntersectionObserver
in einem Lazy-Loading-Szenario, was sollten wir im Auge behalten? Sehen wir uns ein einfaches Beispiel für Lazy-Loading-Bilder an.
Versuchen Sie, diese Seite langsam zum „dritten Bildschirm“ zu scrollen, und beobachten Sie das Überwachungsfenster in der oberen rechten Ecke: Es zeigt Ihnen, wie viele Bilder bisher heruntergeladen wurden.
Im Kern des HTML-Markups für diese Aufgabe liegt eine einfache Bildfolge:
… <img data-src="https://blah-blah.com/foo.jpg"> …
Wie Sie sehen können, sollten die Bilder ohne src
-Tags kommen: Sobald ein Browser das src
-Attribut sieht, wird er sofort damit beginnen, das Bild herunterzuladen, das unseren Absichten widerspricht. Daher sollten wir dieses Attribut nicht auf unsere Bilder in HTML setzen und uns stattdessen auf ein data-
-Attribut wie data-src
hier verlassen.
Ein weiterer Bestandteil dieser Lösung ist natürlich JavaScript. Konzentrieren wir uns hier auf die wesentlichen Punkte:
const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });
Strukturell gibt es hier nichts Neues: Wir haben das alles schon einmal behandelt:
- Wir erhalten alle Nachrichten mit unseren
data-src
Attributen; - Set
config
: Für dieses Szenario möchten Sie Ihren „Erfassungsrahmen“ erweitern, um Elemente zu erkennen, die etwas tiefer als der untere Rand des Ansichtsfensters liegen; - Registrieren
IntersectionObserver
mit dieser Konfiguration; - Iterieren Sie über unsere Bilder und fügen Sie alle hinzu, die von diesem
IntersectionObserver
beobachtet werden sollen;
Der interessante Teil passiert innerhalb der Callback-Funktion, die für die Einträge aufgerufen wird. Es sind drei wesentliche Schritte erforderlich.
Zunächst verarbeiten wir nur die Artikel, die unseren „Erfassungsrahmen“ wirklich schneiden. Dieses Snippet sollte Ihnen inzwischen bekannt sein.
entries.forEach(entry => { if (entry.isIntersecting) { … } });
Dann verarbeiten wir den Eintrag irgendwie, indem wir unser Bild mit
data-src
in ein echtes<img src="…">
umwandeln.if (entry.isIntersecting) { preloadImage(entry.target); … }
preloadImage()
ist eine sehr einfache Funktion, die es hier nicht wert ist, erwähnt zu werden. Lesen Sie einfach die Quelle.Nächster und letzter Schritt: Da Lazy Loading eine einmalige Aktion ist und wir das Bild nicht jedes Mal herunterladen müssen, wenn das Element in unseren „Erfassungsrahmen“ gelangt, sollten wir das bereits verarbeitete Bild nicht mehr
unobserve
. So wie wir es mitelement.removeEventListener()
für unsere regulären Events machen sollten, wenn diese nicht mehr benötigt werden, um Speicherlecks in unserem Code zu verhindern.if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as
self
to our callback self.unobserve(entry.target); }
Notiz. Anstelle von unobserve(event.target)
könnten wir auch disconnect()
aufrufen: es trennt unseren IntersectionObserver
vollständig und würde keine Bilder mehr beobachten. Dies ist nützlich, wenn Sie sich nur um den allerersten Treffer für Ihren Observer kümmern. In unserem Fall brauchen wir den Beobachter, um die Bilder weiter zu überwachen, also sollten wir die Verbindung noch nicht trennen.
Fühlen Sie sich frei, das Beispiel zu verzweigen und mit verschiedenen Einstellungen und Optionen zu spielen. Es gibt jedoch eine interessante Sache zu erwähnen, wenn Sie insbesondere die Bilder faul laden möchten. Sie sollten immer die Box, erzeugt durch das beobachtete Element, im Hinterkopf behalten! Wenn Sie sich das Beispiel ansehen, werden Sie feststellen, dass das CSS für Bilder in den Zeilen 41–47 vermeintlich redundante Stile enthält, inkl. min-height: 100px
. Dies geschieht, um den Bildplatzhaltern ( <img>
ohne src
-Attribut) eine vertikale Dimension zu verleihen. Wozu?
- Ohne vertikale Dimensionen würden alle
<img>
-Tags ein 0×0-Feld erzeugen; - Da das Tag
<img>
standardmäßig eine Artinline-block
Box generiert, werden alle diese 0×0-Boxen nebeneinander in derselben Zeile ausgerichtet; - Dies bedeutet, dass Ihr
IntersectionObserver
alle (oder, je nachdem, wie schnell Sie scrollen, fast alle) Bilder auf einmal registrieren würde – wahrscheinlich nicht ganz das, was Sie erreichen möchten.
Hervorhebung des aktuellen Abschnitts
IntersectionObserver
ist natürlich viel mehr als nur Lazy Loading. Hier ist ein weiteres Beispiel für das Ersetzen von scroll
Ereignissen durch diese Technologie. In diesem Fall haben wir ein ziemlich häufiges Szenario: Auf der festen Navigationsleiste sollten wir den aktuellen Abschnitt basierend auf der Bildlaufposition des Dokuments hervorheben.
Strukturell ähnelt es dem Beispiel für Lazy-Loading-Images und hat die gleiche Grundstruktur mit den folgenden Ausnahmen:
- Jetzt wollen wir nicht Bilder beobachten, sondern die Abschnitte auf der Seite;
- Offensichtlich haben wir auch eine andere Funktion, um die Einträge in unserem Callback zu verarbeiten (
intersectionHandler(entry)
). Aber das hier ist nicht interessant: Alles, was es tut, ist das Umschalten der CSS-Klasse.
Interessant ist hier allerdings das config
Objekt:
const config = { rootMargin: '-50px 0px -55% 0px' };
Warum nicht der Standardwert von 0px
für rootMargin
, fragen Sie? Nun, einfach, weil das Hervorheben des aktuellen Abschnitts und das verzögerte Laden eines Bildes in dem, was wir erreichen wollen, ziemlich unterschiedlich sind. Beim verzögerten Laden möchten wir mit dem Laden beginnen, bevor das Bild in die Ansicht gelangt. Daher haben wir zu diesem Zweck unseren „Erfassungsrahmen“ unten um 50 Pixel verlängert. Wenn wir dagegen den aktuellen Abschnitt hervorheben möchten, müssen wir sicher sein, dass der Abschnitt tatsächlich auf dem Bildschirm sichtbar ist. Und nicht nur das: Wir müssen sicher sein, dass der Benutzer genau diesen Abschnitt tatsächlich liest oder lesen wird. Daher möchten wir, dass ein Abschnitt etwas mehr als die Hälfte des Ansichtsfensters von unten entfernt ist, bevor wir ihn zum aktiven Abschnitt erklären können. Außerdem möchten wir die Höhe der Navigationsleiste berücksichtigen und entfernen daher die Höhe der Leiste aus dem „Erfassungsrahmen“.
Beachten Sie auch, dass wir im Falle des Hervorhebens des aktuellen Navigationselements nicht aufhören möchten, irgendetwas zu beobachten. Hier sollten wir immer den IntersectionObserver
im Auge behalten, daher findet man hier weder disconnect()
noch unobserve()
.
Zusammenfassung
IntersectionObserver
ist eine sehr unkomplizierte Technologie. Es hat eine ziemlich gute Unterstützung in den modernen Browsern und wenn Sie es für Browser implementieren möchten, die es noch (oder überhaupt nicht) unterstützen, gibt es dafür natürlich ein Polyfill. Aber alles in allem ist dies eine großartige Technologie, die es uns ermöglicht, alle möglichen Dinge im Zusammenhang mit der Erkennung von Elementen in einem Ansichtsfenster zu tun und gleichzeitig zu einer wirklich guten Leistungssteigerung beizutragen.
Warum ist IntersectionObserver gut für Sie?
-
IntersectionObserver
ist eine asynchrone, nicht blockierende API! -
IntersectionObserver
ersetzt unsere teuren Listener beiscroll
oderresize
-Events. -
IntersectionObserver
führt all die teuren Berechnungen wiegetClientBoundingRect()
für Sie durch, damit Sie es nicht brauchen. -
IntersectionObserver
folgt dem strukturellen Muster anderer Observer da draußen und sollte daher theoretisch leicht verständlich sein, wenn Sie mit der Funktionsweise anderer Observer vertraut sind.
Dinge, die Sie beachten sollten
Wenn wir die Fähigkeiten von IntersectionObserver mit der Welt von window.addEventListener('scroll')
vergleichen, aus der alles stammt, wird es schwierig sein, irgendwelche Nachteile in diesem Observer zu erkennen. Also, lasst uns stattdessen einige Dinge beachten, die wir beachten sollten:
- Ja,
IntersectionObserver
ist eine asynchrone, nicht blockierende API. Das ist gut zu wissen! Aber es ist noch wichtiger zu verstehen, dass der Code, den Sie in Ihren Rückrufen ausführen, standardmäßig nicht asynchron ausgeführt wird, obwohl die API selbst asynchron ist. Es besteht also immer noch die Möglichkeit, alle Vorteile, die Sie vonIntersectionObserver
erhalten, zu eliminieren, wenn die Berechnungen Ihrer Callback-Funktion dazu führen, dass der Haupt-Thread nicht mehr reagiert. Aber das ist eine andere Geschichte. - Wenn Sie
IntersectionObserver
zum verzögerten Laden der Assets (z. B. Bilder) verwenden, führen.unobserve(asset)
aus, nachdem das Asset geladen wurde. IntersectionObserver
kann Schnittpunkte nur für die Elemente erkennen, die in der Formatierungsstruktur des Dokuments vorkommen. Um es klar zu sagen: Die beobachtbaren Elemente sollten eine Box erzeugen und irgendwie das Layout beeinflussen. Hier nur einige Beispiele zum besseren Verständnis:- Elemente mit
display: none
kommen in Frage; -
opacity: 0
odervisibility:hidden
Erstellen Sie die Box (obwohl unsichtbar), damit diese erkannt werden; - Absolut positionierte Elemente mit
width:0px; height:0px
width:0px; height:0px
sind in Ordnung. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negativetop
,left
, etc.) and are cut out by parent'soverflow: hidden
won't be detected: their box is out of scope for the formatting structure.
- Elemente mit
I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:
- Intersection Observer API on MDN;
- IntersectionObserver polyfill;
- IntersectionObserver polyfill as
npm
module; - Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
- Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.
With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.