Complexité d'un conteneur de défilement infini

Résumé: Réutilisez les éléments DOM et supprimez ceux qui sont éloignés de la fenêtre d'affichage. Utilisez des espaces réservés pour tenir compte des données différées. Voici une démonstration et le code du défilement infini.

Des défilements infinis s'affichent partout sur Internet. La liste des artistes sur Google Music n'en est qu'une, la timeline de Facebook en est une et le flux en direct de Twitter en est aussi une. Vous faites défiler l'écran vers le bas, et avant d'atteindre le bas de l'écran, de nouveaux contenus apparaissent comme par magie. Les utilisateurs bénéficient d'une expérience fluide, et l'attrait est facile à distinguer.

Cependant, le défi technique lié à un conteneur de défilement infini est plus difficile qu'il n'y paraît. Les problèmes que vous rencontrez lorsque vous voulez effectuer The Right ThingTM sont nombreux. Tout commence par des éléments simples tels que les liens dans le pied de page devenant pratiquement inaccessibles, car le contenu ne cesse de le faire disparaître. Mais les problèmes se complexifient. Comment gérer un événement de redimensionnement lorsqu'un utilisateur passe son téléphone du mode portrait au mode paysage, ou comment éviter que votre téléphone ne s'interrompt si la liste devient trop longue ?

La bonne choseTM

Nous avons pensé que c'était une raison suffisante pour proposer une implémentation de référence qui montre un moyen de résoudre tous ces problèmes de manière réutilisable tout en respectant les normes de performances.

Nous allons utiliser trois techniques pour atteindre notre objectif: le recyclage DOM, les tombstones et l'ancrage du défilement.

Notre cas de démonstration sera une fenêtre de chat de type Hangouts où nous pourrons faire défiler les messages. La première chose dont nous avons besoin est une source infinie de messages de chat. Techniquement, aucun des défilements infinis n'est vraiment infini, mais avec la quantité de données disponibles pour être intégrées à ces défilements, ils pourraient l'être aussi. Par souci de simplicité, nous allons simplement coder en dur un ensemble de messages de chat et choisir au hasard le message, l'auteur et les pièces jointes occasionnelles d'une image, avec un peu de retard artificiel pour se comporter un peu plus comme le vrai réseau.

Capture d'écran de l'application Chat

Recyclage DOM

Le recyclage des DOM est une technique sous-utilisée qui permet de limiter le nombre de nœuds DOM. L'idée générale est d'utiliser des éléments DOM déjà créés qui apparaissent hors de l'écran au lieu d'en créer d'autres. Certes, les nœuds DOM eux-mêmes sont bon marché, mais ils ne sont pas sans frais, car chacun d'eux entraîne des coûts supplémentaires en termes de mémoire, de mise en page, de style et de peinture. Les appareils d'entrée de gamme ralentissent sensiblement, voire inutilisables, si le site Web comporte un DOM trop volumineux à gérer. Gardez également à l'esprit que chaque nouvelle mise en page et application de vos styles (un processus déclenché chaque fois qu'une classe est ajoutée ou supprimée d'un nœud) devient plus onéreuse avec un DOM plus grand. Recycler vos nœuds DOM signifie que nous allons réduire considérablement le nombre total de nœuds DOM afin d'accélérer tous ces processus.

Le premier obstacle est le défilement lui-même. Étant donné que nous n'avons qu'un petit sous-ensemble de tous les éléments disponibles dans le DOM à un moment donné, nous devons trouver un autre moyen pour que la barre de défilement du navigateur reflète correctement la quantité de contenu théoriquement utilisée. Nous allons utiliser un élément sentinel de 1 x 1 pixel avec une transformation pour forcer l'élément contenant les éléments (la piste) à avoir la hauteur souhaitée. Nous allons promouvoir chaque élément de la piste dans son propre calque afin que celui-ci soit complètement vide. Pas de couleur d'arrière-plan, rien. Si la couche de la piste n'est pas vide, elle ne peut pas être optimisée par le navigateur. Nous devons donc stocker sur notre carte graphique une texture d'une hauteur de quelques centaines de milliers de pixels. Certainement pas viable sur un appareil mobile.

À chaque défilement, nous vérifions si la fenêtre d'affichage s'est suffisamment proche de la fin de la piste. Si tel est le cas, nous allons prolonger la piste en déplaçant l'élément sentinel et en déplaçant les éléments qui ont quitté la fenêtre d'affichage vers le bas de la piste, en y ajoutant du nouveau contenu.

Runway

Il en va de même pour le défilement dans l'autre direction. Toutefois, nous ne réduirons jamais la piste dans notre implémentation, afin que la position de la barre de défilement reste cohérente.

Pierres tombales

Comme nous l'avons mentionné précédemment, nous essayons de faire en sorte que notre source de données se comporte comme quelque chose du monde réel. Avec la latence du réseau et tout le reste. Cela signifie que si nos utilisateurs ont recours au défilement clignotant, ils peuvent facilement faire défiler le dernier élément pour lequel nous disposons de données. Dans ce cas, nous plaçons un élément tombstone (un espace réservé) qui sera remplacé par l'élément avec le contenu réel une fois les données arrivées. Les tombstones sont également recyclés et disposent d'un pool distinct pour les éléments DOM réutilisables. Nous en avons besoin pour effectuer une bonne transition d'un tombstone à l'élément rempli de contenu, ce qui serait autrement très troublant pour l'utilisateur et pourrait en fait faire perdre la trace de ce sur quoi il se concentrait.

Voilà ce
tombeau. De la pierre. Waouh !

Un défi intéressant ici est que les éléments réels peuvent avoir une hauteur supérieure à l'élément tombstone en raison de différentes quantités de texte par élément ou d'une image jointe. Pour résoudre ce problème, nous ajusterons la position de défilement actuelle chaque fois qu'une donnée arrive et qu'un tombstone est remplacé au-dessus de la fenêtre d'affichage, en ancréant la position de défilement à un élément plutôt qu'à une valeur de pixel. Ce concept est appelé "ancrage du défilement".

Ancrage du défilement

Notre ancrage de défilement sera invoqué lors du remplacement des tombstones et lorsque la fenêtre est redimensionnée (ce qui se produit également lorsque vous retournez les appareils). Nous devons déterminer quel est l'élément le plus visible dans la fenêtre d'affichage. Comme cet élément ne peut être que partiellement visible, nous stockerons également le décalage par rapport à la partie supérieure de l'élément là où la fenêtre d'affichage commence.

Schéma d'ancrage du défilement.

Si la fenêtre d'affichage est redimensionnée et que la piste a changé, nous pouvons restaurer une situation qui semble identique à celle de l'utilisateur. Victoire ! À l'exception d'une fenêtre redimensionnée, cela signifie que chaque élément a potentiellement modifié sa hauteur. Comment savoir à quelle distance du contenu ancré doit être placé ? Non ! Pour le savoir, nous devrions mettre en page chaque élément au-dessus de l'élément ancré et additionner toutes leurs hauteurs. Cela pourrait entraîner une pause significative après un redimensionnement, et cela n'est pas souhaitable. Nous supposons plutôt que chaque élément ci-dessus a la même taille qu'un tombstone et ajustons la position de défilement en conséquence. À mesure que l'utilisateur fait défiler des éléments jusqu'à la piste, nous ajustons la position de défilement, ce qui différencie le travail de mise en page au moment où il est réellement nécessaire.

Mise en page

J'ai ignoré un détail important: la mise en page. Chaque recyclage d'un élément DOM entraîne normalement une remise en page de l'ensemble du défi, ce qui nous met bien en dessous de notre objectif de 60 images par seconde. Pour éviter cela, nous nous chargeons de la mise en page et utilisons des éléments positionnés de manière absolue avec des transformations. De cette façon, nous pouvons faire comme si tous les éléments situés plus haut sur la piste occupent toujours de l'espace alors qu'en réalité, il n'y a que de l'espace vide. Étant donné que nous réalisons nous-mêmes la mise en page, nous pouvons mettre en cache les positions de fin de chaque élément et charger immédiatement le bon élément à partir du cache lorsque l'utilisateur fait défiler la page vers l'arrière.

Idéalement, les éléments ne doivent être repeints qu'une seule fois lorsqu'ils sont attachés au DOM et ne doivent pas être faussés par l'ajout ou la suppression d'autres éléments sur le podium. Cela est possible, mais uniquement avec les navigateurs récents.

Ajustements sur le bord gauche

Récemment, Chrome est compatible avec le conteneur CSS, une fonctionnalité qui permet aux développeurs d'indiquer au navigateur qu'un élément constitue une limite pour le travail de mise en page et de peinture. Comme nous nous chargeons de la mise en page ici, il s'agit d'une application de choix pour le confinement. Chaque fois que nous ajoutons un élément à la piste, nous savons que les autres éléments n'ont pas besoin d'être affectés par la remise en page. Chaque élément doit donc recevoir contain: layout. Comme nous ne voulons pas non plus affecter le reste de notre site Web, la piste elle-même doit également recevoir cette directive de style.

Nous avons également envisagé d'utiliser IntersectionObservers comme mécanisme de détection du moment où l'utilisateur avait fait défiler l'écran suffisamment loin pour que nous puissions commencer à recycler des éléments et charger de nouvelles données. Cependant, les IntersectionObservers sont spécifiés comme ayant une latence élevée (comme si vous utilisiez requestIdleCallback). Nous pouvons donc se sentir moins réactifs avec IntersectionObservers que sans. Même notre implémentation actuelle qui utilise l'événement scroll souffre de ce problème, car les événements de défilement sont envoyés selon la méthode la plus optimale possible. En fin de compte, le Worklet compositeur d'Houdini constituerait la solution haute-fidélité à ce problème.

Ce n'est toujours pas parfait

Notre implémentation actuelle du recyclage des DOM n'est pas idéale, car elle ajoute tous les éléments qui passent à la fenêtre d'affichage, au lieu de se concentrer uniquement sur ceux qui sont à l'écran. Cela signifie que lorsque vous faites défiler très vite le défilement, vous consacrez tellement de travail à la mise en page et à la peinture dans Chrome qu'il ne peut pas suivre le rythme. Vous finirez par ne voir que l'arrière-plan. Ce n'est pas la fin du monde, mais c'est vraiment quelque chose à améliorer.

Nous espérons que vous comprenez à quel point des problèmes simples peuvent devenir difficiles si vous souhaitez combiner une expérience utilisateur de qualité et des normes de performances élevées. Les progressive web apps deviennent des expériences essentielles sur les téléphones mobiles, et cela va prendre une place de plus en plus importante, et les développeurs Web devront continuer à investir dans l'utilisation de modèles qui respectent les contraintes de performances.

Vous trouverez l'intégralité du code dans notre dépôt. Nous avons fait de notre mieux pour qu'il reste réutilisable, mais nous ne le publierons pas en tant que bibliothèque sur npm ni en tant que dépôt distinct. Leur utilisation principale est éducative.