Kurz gesagt: Verwenden Sie Ihre DOM-Elemente wieder und entfernen Sie die Elemente, die weit vom Viewport entfernt sind. Verwenden Sie Platzhalter, um verzögerte Daten zu berücksichtigen. Hier finden Sie eine Demo und hier den Code für den unendlichen Scroller.
Unendliches Scrollen ist im Internet weit verbreitet. Die Künstlerliste von Google Music, die Zeitachse von Facebook und der Live-Feed von Twitter sind jeweils ein Beispiel dafür. Sie scrollen nach unten und bevor Sie das Ende erreichen, erscheinen wie von Zauberhand neue Inhalte. Das ist für Nutzer ein nahtloses Erlebnis und es ist leicht nachzuvollziehen, warum es so beliebt ist.
Die technische Herausforderung hinter einem unendlichen Scroller ist jedoch größer als es scheint. Die Bandbreite der Probleme, die auftreten, wenn Sie das Richtige tun möchten, ist enorm. Das fängt mit einfachen Dingen an, z. B. dass die Links in der Fußzeile praktisch nicht mehr erreichbar sind, weil die Inhalte die Fußzeile immer weiter nach unten schieben. Aber die Probleme werden schwieriger. Wie reagieren Sie auf eine Größenänderung, wenn jemand sein Smartphone vom Hoch- ins Querformat dreht, oder wie verhindern Sie, dass Ihr Smartphone bei einer zu langen Liste schmerzhaft langsam wird?
Das Richtige™
Das war für uns Grund genug, eine Referenzimplementierung zu entwickeln, die zeigt, wie sich all diese Probleme auf wiederverwendbare Weise lösen lassen, ohne die Leistungsstandards zu beeinträchtigen.
Wir verwenden drei Techniken, um unser Ziel zu erreichen: DOM-Recycling, Tombstones und Scroll-Anchoring.
Unser Demofall ist ein Hangouts-ähnliches Chatfenster, in dem wir durch die Nachrichten scrollen können. Als Erstes benötigen wir eine unendliche Quelle für 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önnten sie es auch sein. Der Einfachheit halber werden wir nur eine Reihe von Chatnachrichten fest codieren und Nachricht, Autor und gelegentliche Bildanhänge nach dem Zufallsprinzip auswählen. Außerdem fügen wir eine künstliche Verzögerung ein, damit es sich etwas mehr wie das echte Netzwerk verhält.

DOM-Recycling
DOM-Recycling ist eine untergenutzte Technik, um die Anzahl der DOM-Knoten gering zu halten. Die allgemeine Idee besteht darin, bereits erstellte DOM-Elemente zu verwenden, die sich außerhalb des Bildschirms befinden, anstatt neue zu erstellen. DOM-Knoten selbst sind zwar günstig, aber nicht kostenlos, da jeder von ihnen zusätzliche Kosten für Arbeitsspeicher, Layout, Stil und Rendering verursacht. Auf Low-End-Geräten wird die Leistung deutlich schlechter, wenn die Website ein zu großes DOM hat. Im schlimmsten Fall ist die Website dann nicht mehr nutzbar. Denken Sie auch daran, dass jedes Neulayout und jede erneute Anwendung Ihrer Stile – ein Prozess, der immer dann ausgelöst wird, wenn einem Knoten eine Klasse hinzugefügt oder daraus entfernt wird – mit einem größeren DOM teurer wird. Durch das Wiederverwenden von DOM-Knoten wird die Gesamtzahl der DOM-Knoten erheblich reduziert, wodurch alle diese Prozesse beschleunigt werden.
Die erste Hürde ist das Scrollen selbst. Da wir zu einem bestimmten Zeitpunkt nur eine kleine Teilmenge aller verfügbaren Elemente im DOM haben, müssen wir einen anderen Weg finden, damit die Scrollleiste des Browsers die Menge an Inhalten, die theoretisch vorhanden sind, richtig widerspiegelt. Wir verwenden ein 1 × 1 Pixel großes Sentinel-Element mit einer Transformation, um das Element, das die Artikel enthält – den Laufsteg – auf die gewünschte Höhe zu bringen. Wir verschieben jedes Element auf dem Laufsteg auf eine eigene Ebene, damit die Ebene des Laufstegs selbst vollständig leer ist. Keine Hintergrundfarbe, nichts. Wenn die Ebene der Start- und Landebahn nicht leer ist, kommt sie nicht für die Optimierungen des Browsers infrage und wir müssen eine Textur auf unserer Grafikkarte speichern, die eine Höhe von mehreren Hunderttausend Pixeln hat. Auf einem Mobilgerät ist das definitiv nicht möglich.
Bei jedem Scrollen wird geprüft, ob der Viewport dem Ende des Laufstegs nahe genug ist. In diesem Fall verlängern wir die Runway, indem wir das Sentinel-Element und die Elemente, die den Viewport verlassen haben, an das Ende der Runway verschieben und mit neuen Inhalten füllen.
Dasselbe gilt für das Scrollen in die andere Richtung. Wir werden die Runway in unserer Implementierung jedoch niemals verkleinern, damit die Position der Scrollleiste gleich bleibt.
Tombstones
Wie bereits erwähnt, versuchen wir, unsere Datenquelle so zu gestalten, dass sie sich wie etwas in der realen Welt verhält. Mit Netzwerklatenz und allem. Wenn unsere Nutzer also die Flick-Scrolling-Funktion verwenden, können sie problemlos am letzten Element, für das wir Daten haben, vorbeiscrollen. In diesem Fall wird ein Platzhalter eingefügt, der durch das Element mit den tatsächlichen Inhalten ersetzt wird, sobald die Daten eingegangen sind. Tombstones werden ebenfalls wiederverwendet und haben einen separaten Pool für wiederverwendbare DOM-Elemente. Das ist wichtig, damit wir einen reibungslosen Übergang von einem Platzhalter zum mit Inhalten gefüllten Element schaffen können. Andernfalls wäre das für den Nutzer sehr störend und er würde möglicherweise den Fokus verlieren.

Eine interessante Herausforderung besteht darin, dass echte Elemente aufgrund unterschiedlicher Textmengen pro Element oder eines angehängten Bildes eine größere Höhe als das Platzhalterelement haben können. Um dieses Problem zu beheben, passen wir die aktuelle Scrollposition jedes Mal an, wenn Daten eingehen und ein Platzhalter über dem Viewport ersetzt wird. Dabei wird die Scrollposition an ein Element und nicht an einen Pixelwert verankert. Dieses Konzept wird als Scroll-Anker bezeichnet.
Scroll-Anker
Die Scrollverankerung wird sowohl beim Ersetzen von Tombstones als auch beim Ändern der Fenstergröße aufgerufen (was auch beim Drehen des Geräts passiert). Wir müssen herausfinden, welches das oberste sichtbare Element im Darstellungsbereich ist. Da dieses Element möglicherweise nur teilweise sichtbar ist, speichern wir auch den Offset vom oberen Rand des Elements, an dem der Darstellungsbereich beginnt.

Wenn die Größe des Viewports geändert wird und sich die Runway ändert, können wir eine Situation wiederherstellen, die für den Nutzer visuell identisch ist. Gewonnen! Wenn ein Fenster in der Größe geändert wird, ändert sich möglicherweise die Höhe der einzelnen Elemente. Wie können wir also wissen, wie weit unten die verankerten Inhalte platziert werden sollen? Das tun wir nicht. Um das herauszufinden, müssten wir jedes Element über dem verankerten Element anordnen und alle Höhen addieren. Dies könnte nach einer Größenänderung zu einer erheblichen Pause führen, was wir vermeiden möchten. Stattdessen gehen wir davon aus, dass jedes Element oben die gleiche Größe wie ein Tombstone hat, und passen unsere Scrollposition entsprechend an. Wenn Elemente in den Runway gescrollt werden, passen wir die Scrollposition an und verschieben die Layoutarbeit effektiv auf den Zeitpunkt, an dem sie tatsächlich benötigt wird.
Layout
Ich habe ein wichtiges Detail ausgelassen: das Layout. Bei jedem Recycling eines DOM-Elements würde normalerweise das gesamte Laufband neu angeordnet werden, was uns weit unter unser Ziel von 60 Frames pro Sekunde bringen würde. Um dies zu vermeiden, übernehmen wir die Last des Layouts selbst und verwenden absolut positionierte Elemente mit Transformationen. So können wir so tun, als würden alle Elemente weiter oben auf der Start- und Landebahn immer noch Platz einnehmen, obwohl es sich in Wirklichkeit nur um leeren Raum handelt. Da wir das Layout selbst erstellen, können wir die Positionen, an denen die einzelnen Elemente landen, im Cache speichern und das richtige Element sofort aus dem Cache laden, wenn der Nutzer zurückscrollt.
Im Idealfall werden Elemente nur einmal neu gezeichnet, wenn sie an das DOM angehängt werden, und sind nicht von Hinzufügungen oder Entfernungen anderer Elemente im Laufband betroffen. Das ist möglich, aber nur mit modernen Browsern.
Neueste Optimierungen
Vor Kurzem wurde Chrome um die Unterstützung für CSS-Containment erweitert. Mit dieser Funktion können Entwickler dem Browser mitteilen, dass ein Element eine Grenze für Layout- und Renderingvorgänge ist. Da wir das Layout hier selbst erstellen, ist das ein Paradebeispiel für die Eindämmung. Wenn wir ein Element in den Runway einfügen, wissen wir, dass die anderen Elemente nicht von der Neugestaltung betroffen sein müssen. Jedes Element sollte also contain: layout
sein. Wir möchten auch nicht, dass sich das auf den Rest unserer Website auswirkt. Daher sollte auch der Runway diese Stilanweisung erhalten.
Eine weitere Möglichkeit, die wir in Betracht gezogen haben, ist die Verwendung von IntersectionObservers
, um zu erkennen, wann der Nutzer weit genug gescrollt hat, damit wir mit dem Wiederverwenden von Elementen beginnen und neue Daten laden können. IntersectionObservers sind jedoch für eine hohe Latenz ausgelegt (als ob requestIdleCallback
verwendet würde). Daher kann es sein, dass die Reaktionsfähigkeit mit IntersectionObservers geringer ist als ohne. Auch unsere aktuelle Implementierung mit dem scroll
-Ereignis ist von diesem Problem betroffen, da Scroll-Ereignisse nach dem „Best Effort“-Prinzip gesendet werden. Letztendlich wäre das Houdini Compositor Worklet die Lösung für dieses Problem.
Es ist noch nicht perfekt
Unsere aktuelle Implementierung von DOM-Recycling ist nicht ideal, da alle Elemente hinzugefügt werden, die durch den Anzeigebereich laufen, anstatt nur die Elemente zu berücksichtigen, die tatsächlich auf dem Bildschirm angezeigt werden. Wenn Sie also sehr schnell scrollen, muss Chrome so viel für Layout und Darstellung leisten, dass es nicht mithalten kann. Sie sehen dann nur noch den Hintergrund. Das ist nicht das Ende der Welt, aber definitiv etwas, das verbessert werden muss.
Wir hoffen, dass Sie sehen, wie schwierig einfache Probleme werden können, wenn Sie eine hohe Nutzerfreundlichkeit mit hohen Leistungsstandards kombinieren möchten. Da progressive Web-Apps auf Smartphones immer wichtiger werden, wird dies noch wichtiger und Webentwickler müssen weiterhin in die Verwendung von Mustern investieren, die Leistungsbeschränkungen berücksichtigen.
Der gesamte Code ist in unserem Repository verfügbar. Wir haben unser Bestes getan, um es wiederverwendbar zu machen, werden es aber nicht als tatsächliche Bibliothek auf npm oder als separates Repository veröffentlichen. Die primäre Nutzung ist für Bildungszwecke.