Zusammenfassung: Verwenden Sie Ihre DOM-Elemente wieder und entfernen Sie die, die weit vom Darstellungsbereich entfernt sind. Verwenden Sie Platzhalter, um verzögerte Daten zu berücksichtigen. Hier finden Sie eine Demo und den Code für den endlosen Scroller.
Unendliche Scroller tauchen überall im Internet auf. Die Künstlerliste von Google Music ist eine, die Zeitachse von Facebook ist eine und der Livefeed von Twitter ist eine. Sie scrollen nach unten und bevor Sie das Ende erreichen, erscheinen neue Inhalte wie aus dem Nichts. Die Nutzung ist für Nutzer nahtlos und die Vorteile liegen auf der Hand.
Die technische Herausforderung hinter einem endlosen Scroller ist jedoch schwieriger, als es scheint. Die Bandbreite der Probleme, die auftreten, wenn Sie das Richtige tun möchten, ist enorm. Es beginnt mit einfachen Dingen, z. B. dass die Links in der Fußzeile praktisch nicht mehr erreichbar sind, weil Inhalte die Fußzeile immer weiter nach unten schieben. 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 zu langsam wird, wenn die Liste zu lang wird?
The right thing™
Wir fanden das Grund genug, eine Referenzimplementierung zu entwickeln, die zeigt, wie sich all diese Probleme auf wiederverwendbare Weise angehen lassen, während die Leistungsstandards eingehalten werden.
Wir verwenden drei Techniken, um unser Ziel zu erreichen: DOM-Recycling, Tombstones und Scroll-Ankern.
Unser Demofall ist ein Hangouts-ähnliches Chatfenster, in dem wir durch die Nachrichten scrollen können. Als Erstes benötigen wir eine unendliche Quelle von Chatnachrichten. Technisch gesehen ist keiner der unendlichen Scroller wirklich unendlich, aber angesichts der Menge an Daten, die in diese Scroller eingespeist werden können, könnte man das fast meinen. Der Einfachheit halber werden wir einfach eine Reihe von Chatnachrichten hartcodieren und Nachricht, Autor und gelegentliche Bildanhänge nach dem Zufallsprinzip mit einer künstlichen Verzögerung auswählen, damit sich das Netzwerk ein wenig mehr wie das echte Netzwerk verhält.

DOM-Recycling
Das DOM-Recycling ist eine wenig genutzte Methode, um die Anzahl der DOM-Knoten niedrig zu halten. Im Allgemeinen sollten Sie bereits erstellte DOM-Elemente verwenden, die sich außerhalb des Bildschirms befinden, anstatt neue zu erstellen. DOM-Knoten sind zwar kostengünstig, aber nicht kostenlos, da jeder von ihnen zusätzliche Kosten für Arbeitsspeicher, Layout, Stil und Darstellung verursacht. Low-End-Geräte werden merklich langsamer, wenn nicht sogar unbrauchbar, wenn die Website ein zu großes DOM hat. Denken Sie auch daran, dass jedes Neulayout und jede Neuanwendung Ihrer Stile – ein Vorgang, der ausgelöst wird, wenn einer Klasse ein Knoten hinzugefügt oder von einem Knoten entfernt wird – mit einem größeren DOM immer aufwendiger wird. Durch das Recycling der DOM-Knoten können wir die Gesamtzahl der DOM-Knoten deutlich reduzieren und so alle diese Prozesse beschleunigen.
Die erste Hürde ist das Scrollen selbst. Da wir zu jedem Zeitpunkt nur einen kleinen Teil aller verfügbaren Elemente im DOM haben, müssen wir eine andere Möglichkeit finden, die Scrollbar des Browsers so zu gestalten, dass sie die Menge der Inhalte korrekt widerspiegelt, die theoretisch vorhanden sind. Wir verwenden ein Sentinel-Element mit einer Größe von 1 × 1 Pixel mit einer Transformation, um das Element, das die Elemente enthält (die Landebahn), auf die gewünschte Höhe zu zwingen. Wir heben jedes Element auf der Landebahn in eine eigene Ebene hervor, damit die Ebene der Landebahn selbst vollständig leer ist. Keine Hintergrundfarbe, nichts. Wenn die Ebene der Landebahn nicht leer ist, kann sie nicht für die Optimierungen des Browsers verwendet werden. Wir müssen dann eine Textur mit einer Höhe von mehreren hunderttausend Pixeln auf unserer Grafikkarte speichern. Auf einem Mobilgerät ist das definitiv nicht möglich.
Beim Scrollen wird geprüft, ob der Viewport dem Ende der Landebahn ausreichend nahe gekommen ist. In diesem Fall verlängern wir den Vorlauf, indem wir das Sentinel-Element verschieben und die Elemente, die den Darstellungsbereich verlassen haben, an den unteren Rand des Vorlaufs verschieben und mit neuen Inhalten füllen.
Das Gleiche gilt für das Scrollen in die andere Richtung. Wir werden die Landebahn in unserer Implementierung jedoch niemals verkleinern, damit die Position der Bildlaufleiste gleich bleibt.
Grabsteine
Wie bereits erwähnt, versuchen wir, unsere Datenquelle so zu gestalten, dass sie sich wie eine reale Quelle verhält. Mit Netzwerklatenz und allem. Wenn Nutzer also wischen, können sie leicht über das letzte Element scrollen, für das wir Daten haben. In diesem Fall fügen wir ein Tombstone-Element ein, also einen Platzhalter, der durch das Element mit den tatsächlichen Inhalten ersetzt wird, sobald die Daten eingegangen sind. Tombstones werden ebenfalls recycelt und es gibt einen separaten Pool für wiederverwendbare DOM-Elemente. So können wir einen fließenden Übergang von einem Grabstein zum Element mit Inhalten schaffen, was sonst sehr irritierend für den Nutzer wäre und ihn möglicherweise dazu bringen könnte, den Überblick zu verlieren.

Eine interessante Herausforderung besteht darin, dass echte Elemente aufgrund unterschiedlicher Textmengen pro Element oder eines angehängten Bilds eine größere Höhe als das Grabsteinelement haben können. Um dieses Problem zu beheben, passen wir die aktuelle Scrollposition jedes Mal an, wenn Daten eingehen und ein Grabstein über dem Viewport ersetzt wird. Dabei wird die Scrollposition an einem Element und nicht an einem Pixelwert verankert. Dieses Konzept wird als Scroll-Ankern bezeichnet.
Scrollankern
Die Scrollanpassung wird sowohl beim Ersetzen von Grabsteinen als auch beim Ändern der Fenstergröße aufgerufen. Das passiert auch, wenn das Gerät gedreht wird. Wir müssen herausfinden, welches das oberste sichtbare Element im Darstellungsbereich ist. Da dieses Element nur teilweise sichtbar sein kann, speichern wir auch den Offset vom oberen Rand des Elements, an dem der Darstellungsbereich beginnt.

Wenn die Größe des Darstellungsbereichs geändert wird und sich die Landebahn verändert hat, können wir eine Situation wiederherstellen, die für den Nutzer optisch identisch erscheint. Gewonnen! Wenn das Fenster jedoch neu skaliert wird, hat sich möglicherweise auch die Höhe der einzelnen Elemente geändert. Woher wissen wir dann, wie weit unten die verankerten Inhalte platziert werden sollten? Nein, das tun wir nicht. Um das herauszufinden, müssten wir jedes Element über dem angedockten Element layouten und alle Höhen addieren. Das könnte nach einer Größenänderung zu einer erheblichen Verzögerung führen, was wir nicht möchten. Stattdessen gehen wir davon aus, dass jedes Element oben dieselbe Größe wie ein Grabstein hat, und passen die Scrollposition entsprechend an. Wenn Elemente in den Runway gescrollt werden, passen wir die Scrollposition an und verschieben die Layoutarbeit sozusagen auf den Zeitpunkt, zu dem sie tatsächlich benötigt wird.
Layout
Ich habe ein wichtiges Detail übersprungen: das Layout. Bei jedem Recycling eines DOM-Elements würde normalerweise das gesamte Runway neu layoutet, was weit unter unserem Ziel von 60 Frames pro Sekunde liegen würde. Um dies zu vermeiden, übernehmen wir die Layoutarbeit und verwenden absolut positionierte Elemente mit Transformationen. So können wir so tun, als würden alle Elemente weiter oben auf der Landebahn noch Platz einnehmen, obwohl dort in Wirklichkeit nur leerer Raum ist. Da wir das Layout selbst erstellen, können wir die Positionen, an denen sich die einzelnen Elemente befinden, im Cache speichern und das richtige Element sofort aus dem Cache laden, wenn der Nutzer zurückscrollt.
Im Idealfall werden Elemente nur einmal neu gerendert, wenn sie an das DOM angehängt werden, und sind von Hinzufügungen oder Entfernungen anderer Elemente in der Laufzeit nicht betroffen. Das ist möglich, aber nur mit modernen Browsern.
Innovative Optimierungen
Kürzlich wurde in Chrome CSS-Begrenzung unterstützt. Mit dieser Funktion können Entwickler dem Browser mitteilen, dass ein Element eine Grenze für das Layout und die Malarbeit ist. Da wir das Layout hier selbst erstellen, ist dies eine ideale Anwendung für die Begrenzung. Wenn wir der Start-/Landebahn ein Element hinzufügen, wissen wir, dass die anderen Elemente nicht vom Neulayout betroffen sein müssen. Daher sollte für jeden Artikel contain: layout
zurückgegeben werden. Wir möchten auch nicht, dass sich die Änderung auf den Rest der Website auswirkt. Daher sollte diese Stilrichtlinie auch für die Landebahn selbst gelten.
Wir haben auch überlegt, IntersectionObservers
als Mechanismus zu verwenden, um zu erkennen, wann der Nutzer weit genug gescrollt hat, damit wir Elemente wiederverwenden und neue Daten laden können. IntersectionObservers haben jedoch eine hohe Latenz (wie bei der Verwendung von requestIdleCallback
), sodass die Anwendung mit IntersectionObservers scheinbar weniger reaktionsschnell ist als ohne. Auch unsere aktuelle Implementierung mit dem Ereignis scroll
leidet unter diesem Problem, da Scrollereignisse nach dem Best-Effort-Prinzip gesendet werden. Letztendlich war das Compositor-Worklet von Houdini die Lösung für dieses Problem.
Es ist noch nicht perfekt
Unsere aktuelle Implementierung des DOM-Recyclings ist nicht ideal, da alle Elemente hinzugefügt werden, die den Viewport durchlaufen, anstatt nur die, die sich tatsächlich auf dem Bildschirm befinden. Wenn Sie also sehr schnell scrollen, wird Chrome so stark beansprucht, dass es nicht mehr mithalten kann. Sie sehen dann nur noch den Hintergrund. Das ist zwar nicht weiter schlimm, sollte aber unbedingt verbessert werden.
Wir hoffen, dass Sie jetzt nachvollziehen können, wie schwierig einfache Probleme werden können, wenn Sie eine hohe Nutzerfreundlichkeit mit hohen Leistungsstandards kombinieren möchten. Da progressive Web-Apps immer wichtiger werden, müssen Webentwickler weiterhin in die Verwendung von Mustern investieren, die Leistungseinschränkungen berücksichtigen.
Den gesamten Code finden Sie in unserem Repository. Wir haben unser Bestes getan, um sie wiederverwendbar zu machen, werden sie aber nicht als Bibliothek auf npm oder als separates Repository veröffentlichen. Der Hauptzweck ist die Aufklärung.