Die Komplexität von unendlichem Scrollen

Zusammenfassung: Sie können Ihre DOM-Elemente wiederverwenden und diejenigen entfernen, die weit vom Darstellungsbereich entfernt sind. Verwenden Sie Platzhalter, um verzögerte Daten zu berücksichtigen. Hier findest du eine Demo und den Code für unendliches Scrollen.

Unendliche Scroller tauchen überall im Internet auf. Die Künstlerliste von Google Music ist eins, die Zeitachse bei Facebook und der Live-Feed von Twitter. Sie scrollen nach unten und bevor Sie am unteren Ende angelangt sind, erscheinen wie von Zauberhand neue Inhalte wie aus dem Nichts. Die Nutzererfahrung ist nahtlos und die Attraktivität ist offensichtlich.

Die technische Herausforderung hinter einem unendlichen Scroller ist jedoch schwieriger, als es scheint. Bei der Arbeit mit The Right ThingTM können sehr viele Probleme auftreten. Zuerst werden einfache Dinge wie die Links in der Fußzeile praktisch unerreichbar, weil die Fußzeile durch Inhalte ständig weggeschoben wird. Aber die Probleme werden schwieriger. Wie gehen Sie mit einem Größenänderungsereignis um, wenn jemand sein Smartphone vom Hoch- ins Querformat dreht oder wie verhindern Sie, dass Ihr Smartphone plötzlich zum Stillstand bricht, wenn die Liste zu lang wird?

Die richtige SacheTM

Wir hielten dies für ausreichend, eine Referenzimplementierung zu entwickeln, die eine Möglichkeit zeigt, all diese Probleme auf wiederverwendbare Weise zu lösen und gleichzeitig Leistungsstandards einzuhalten.

Wir verwenden drei Techniken, um unser Ziel zu erreichen: DOM-Recycling, Tombstones und Scroll Verankerung.

Unser Demo-Fall wird ein Hangouts-ähnliches Chatfenster sein, in dem wir durch die Nachrichten scrollen können. Das Erste, was wir brauchen, ist eine unendliche Quelle für Chat-Nachrichten. Technisch gesehen gibt es keinen der unendlichen Scroller wirklich unendlich, aber bei der Menge an Daten, die zur Verfügung steht, um in diese Scroller gepumpt zu werden, könnte das genauso sein. Der Einfachheit halber werden wir eine Reihe von Chatnachrichten einfach hartcodieren und Nachrichten, Autor und gelegentliche Bildanhänge mit einer kleinen künstlichen Verzögerung auswählen, um sich ein wenig mehr wie das echte Netzwerk zu verhalten.

Screenshot der Chat-App

DOM-Recycling

Das DOM-Recycling ist eine nicht ausgelastete Technik, um die Anzahl der DOM-Knoten niedrig zu halten. Im Allgemeinen sollten Sie bereits erstellte DOM-Elemente verwenden, die sich außerhalb des sichtbaren Bildschirmbereichs befinden, anstatt neue Elemente zu erstellen. Zugegebenermaßen sind DOM-Knoten selbst günstig, aber sie sind nicht kostenlos, da sie zusätzliche Kosten für Arbeitsspeicher, Layout, Stil und Paint verursachen. Low-End-Geräte werden deutlich langsamer, wenn sie nicht komplett unbrauchbar sind, wenn die Website ein zu großes DOM zur Verwaltung hat. Beachten Sie auch, dass jede Neuanordnung und Neuanwendung Ihrer Stile – ein Prozess, der ausgelöst wird, wenn eine Klasse einem Knoten hinzugefügt oder von einem Knoten entfernt wird – mit einem größeren DOM teurer wird. Durch das Recyceln der DOM-Knoten wird die Gesamtzahl der DOM-Knoten erheblich reduziert, wodurch all diese Prozesse beschleunigt werden.

Die erste Hürde ist das Scrollen selbst. Da immer nur eine kleine Teilmenge aller verfügbaren Elemente im DOM vorhanden ist, müssen wir eine andere Möglichkeit finden, damit die Bildlaufleiste des Browsers die theoretisch vorhandene Menge des Inhalts korrekt widerspiegelt. Wir verwenden ein 1 x 1 Pixel großes Sentinel-Element mit einer Transformation, um zu erzwingen, dass das Element, das die Objekte enthält – die Startbahn – die gewünschte Höhe hat. Wir stufen jedes Element auf der Startbahn in eine eigene Ebene hoch, damit diese Ebene vollständig leer ist. Keine Hintergrundfarbe, nichts. Wenn die Startbahnebene nicht leer ist, kommt sie nicht für die Browseroptimierungen infrage und wir müssen eine Textur auf unserer Grafikkarte mit einer Höhe von einigen hunderttausend Pixeln speichern. Auf einem Mobilgerät auf jeden Fall nicht praktikabel.

Beim Scrollen prüfen wir, ob sich der Darstellungsbereich ausreichend an das Ende der Start- und Landebahn rückt. In diesem Fall verlängern wir die Startbahn, indem wir das Sentinel-Element verschieben und die Elemente, die den Darstellungsbereich verlassen haben, an den unteren Rand der Startbahn verschieben und diese mit neuen Inhalten füllen.

Landebahn Sentinel

Dasselbe gilt für das Scrollen in die andere Richtung. Die Startbahn wird in unserer Implementierung jedoch nie verkleinert, damit die Position der Bildlaufleiste konsistent bleibt.

Grabsteine

Wie bereits erwähnt, versuchen wir, dafür zu sorgen, dass sich die Datenquelle wie in der realen Welt verhält. Mit Netzwerklatenz und allem. Das bedeutet, dass Nutzer, die Flicky-Scrolling verwenden, einfach an dem letzten Element vorbeiscrollen können, für das uns Daten vorliegen. In diesem Fall platzieren wir ein Tombstone-Element – einen Platzhalter –, der durch den tatsächlichen Inhalt ersetzt wird, sobald die Daten eintreffen. Tombstones werden ebenfalls recycelt und haben einen separaten Pool für wiederverwendbare DOM-Elemente. Wir brauchen das, damit wir einen schönen Übergang von einem Tombstone-Element zu einem mit Inhalten gefüllten Element vornehmen können, was für den Nutzer sehr irritiert wäre und tatsächlich dazu führen könnte, dass er den Überblick verliert, worauf er sich fokussiert hat.

Ein solches Grab. Sehr steinig. Wow.

Eine interessante Herausforderung besteht darin, dass echte Elemente aufgrund der unterschiedlichen Textmenge pro Objekt oder angehängtes Bild eine größere Höhe als das Tombstone-Element haben können. Um dieses Problem zu beheben, wird die aktuelle Scrollposition jedes Mal angepasst, wenn Daten eingehen und ein Tombstone über dem Darstellungsbereich ersetzt wird. Dabei wird die Scrollposition mit einem Element und nicht mit einem Pixelwert verankert. Dieses Konzept wird als Scrollverankerung bezeichnet.

Verankerung durch Scrollen

Die Scroll-Verankerung wird sowohl beim Ersetzen von Tombstones als auch beim Ändern der Fenstergröße ausgelöst (was auch passiert, wenn die Geräte umgedreht werden). Wir müssen herausfinden, was das oberste sichtbare Element im Darstellungsbereich ist. Da dieses Element nur teilweise sichtbar ist, speichern wir auch den Abstand vom oberen Rand des Elements, mit dem der Darstellungsbereich beginnt.

Verankerungsdiagramm scrollen

Wenn die Größe des Darstellungsbereichs angepasst wird und sich die Startbahn verändert, können wir eine Situation wiederherstellen, die dem Nutzer optisch gleich erscheint. Gewonnen! Außer bei einem größenangepassten Fenster bedeutet dies, dass möglicherweise die Höhe jedes Elements geändert wurde. Wie wissen wir also, wie weit unten der verankerte Inhalt platziert werden soll? Nein. Um herauszufinden, ob wir jedes Element oberhalb des verankerten Elements in das Layout aufnehmen und alle Höhen addieren müssen, könnte dies nach einer Größenänderung zu einer erheblichen Pause führen, was wir nicht möchten. Stattdessen gehen wir davon aus, dass jedes obige Element die gleiche Größe wie ein Tombstone hat, und passen die Scrollposition entsprechend an. Wenn Elemente auf den Laufsteg gescrollt werden, passen wir die Scrollposition an und verschieben die Layoutarbeit so, dass sie tatsächlich benötigt wird.

Layout

Ein wichtiges Detail habe ich übersprungen: das Layout. Bei jeder Wiederverwertung eines DOM-Elements würde das Layout der gesamten Startbahn normalerweise neu angeordnet werden. Damit würden wir unsere Zielvorgabe von 60 Bildern pro Sekunde deutlich unterschreiten. Um dies zu vermeiden, übernehmen wir die Last des Layouts selbst und verwenden absolut positionierte Elemente mit Transformationen. Auf diese Weise können wir so tun, als würden alle Elemente weiter oben auf der Startbahn immer noch Platz einnehmen, obwohl in Wirklichkeit nur freie Fläche vorhanden ist. Da wir das Layout selbst durchführen, können wir die Positionen, an denen jedes Element endet, im Cache speichern. Außerdem können wir sofort das richtige Element aus dem Cache laden, wenn der Nutzer rückwärts scrollt.

Im Idealfall werden Elemente nur einmal neu gestrichen, wenn sie mit dem DOM verbunden werden. Das Hinzufügen oder Entfernen anderer Objekte auf der Start- und Landebahn hat keine Beeinträchtigung. Das ist möglich, aber nur mit modernen Browsern.

bahnbrechende Optimierungen

Seit Kurzem unterstützt Chrome CSS-Einschränkungen, eine Funktion, mit der Entwickler dem Browser mitteilen können, dass ein Element eine Grenze für Layout und Painting darstellt. Da wir das Layout hier selbst erstellen, ist es eine wichtigste Anwendung für die Begrenzung. Immer wenn wir dem Laufsteg ein Element hinzufügen, wissen wir, dass die anderen Elemente nicht von der Umgestaltung betroffen sein müssen. Jedes Element sollte also contain: layout lauten. Wir möchten auch keine Auswirkungen auf den Rest unserer Website haben, daher sollte die Startbahn selbst diese Stilanweisung erhalten.

Wir haben auch IntersectionObservers als Mechanismus in Betracht gezogen, um zu erkennen, wann der Nutzer weit genug gescrollt ist, damit wir Elemente recycelt und neue Daten laden können. IntersectionObservers haben jedoch eine hohe Latenz (wie bei der Verwendung von requestIdleCallback), sodass wir mit IntersectionObservers eventuell weniger reaktionsschnell fühlen als ohne. Dieses Problem tritt auch bei unserer aktuellen Implementierung mit dem scroll-Ereignis auf, da Scroll-Ereignisse nach dem Best-Effort-Prinzip ausgelöst werden. Letztendlich wäre das Houdini’s Compositor Worklet die High-Fidelity-Lösung für dieses Problem.

Noch nicht perfekt

Unsere derzeitige Implementierung der DOM-Recycling ist nicht ideal, da alle Elemente hinzugefügt werden, die den Darstellungsbereich durchpassen, und nicht nur die Elemente, die tatsächlich auf dem Bildschirm angezeigt werden. Wenn du also wirklich schnell scrollst, steckst du in Chrome so viel Arbeit in das Layout und die Farbe, dass Chrome nicht mehr mithalten kann. Am Ende siehst du nur den Hintergrund. Es ist nicht das Ende der Welt, aber es muss verbessert werden.

Wir hoffen, dass Sie sehen, wie schwierig einfache Probleme werden können, wenn Sie eine gute Nutzererfahrung mit hohen Leistungsstandards kombinieren möchten. Da progressive Web-Apps zu Kernfunktionen auf Smartphones werden, wird dies immer wichtiger. Webentwickler müssen weiterhin in Muster investieren, die Leistungseinschränkungen berücksichtigen.

Der gesamte Code befindet sich in unserem Repository. Wir haben uns bemüht, sie wiederverwendbar zu halten, werden sie aber nicht als tatsächliche Bibliothek auf npm oder als separates Repository veröffentlichen. Der Hauptzweck ist die Bildung.