Résumé: Réutilisez vos é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 retardées. Voici une démonstration et le code du défilement infini.
Les défilements infinis apparaissent partout sur Internet. La liste d'artistes de Google Music est une, celle de Facebook est une et celle du flux Twitter est également une. Vous faites défiler la page vers le bas et, avant d'atteindre le bas, de nouveaux contenus apparaissent comme par magie, semblant surgir de nulle part. L'expérience est fluide pour les utilisateurs, et l'attrait est évident.
Cependant, le défi technique derrière un défilement infini est plus difficile qu'il n'y paraît. La gamme de problèmes que vous rencontrez lorsque vous voulez faire la bonne chose™ est vaste. Cela commence par des choses simples, comme les liens du pied de page qui deviennent pratiquement inaccessibles, car le contenu continue de les repousser. Mais les problèmes deviennent plus difficiles. Comment gérer un événement de redimensionnement lorsqu'un utilisateur passe son téléphone du mode portrait au mode paysage ou comment empêcher votre téléphone de s'arrêter brutalement lorsque la liste devient trop longue ?
The Right Thing™
Nous avons pensé que cela était suffisant pour proposer une implémentation de référence qui montre comment 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 du DOM, les pierres tombales et l'ancrage du défilement.
Notre cas de démonstration sera une fenêtre de chat semblable à Hangouts, dans laquelle 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éfileurs infinis disponibles n'est vraiment infini, mais avec la quantité de données disponibles à injecter dans ces défileurs, ils pourraient tout aussi bien l'être. Pour simplifier, nous allons simplement coder en dur un ensemble de messages de chat et choisir le message, l'auteur et une pièce jointe d'image occasionnelle de manière aléatoire, avec un peu de retard artificiel pour se comporter un peu plus comme le réseau réel.

Recyclage du DOM
Le recyclage du DOM est une technique sous-utilisée pour réduire le nombre de nœuds DOM. L'idée générale est d'utiliser des éléments DOM déjà créés qui sont hors écran au lieu d'en créer. Certes, les nœuds DOM eux-mêmes sont peu coûteux, mais ils ne sont pas sans frais, car chacun d'eux ajoute des coûts supplémentaires en termes de mémoire, de mise en page, de style et de peinture. Les appareils bas de gamme seront nettement plus lents, voire inutilisables, si le site Web comporte un DOM trop volumineux à gérer. N'oubliez pas non plus que chaque redisposition et ré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 coûteuse avec un DOM plus volumineux. En recyclant vos nœuds DOM, nous allons réduire considérablement le nombre total de nœuds DOM, ce qui accélérera tous ces processus.
Le premier obstacle est le défilement lui-même. Étant donné que nous n'aurons qu'un sous-ensemble minuscule de tous les éléments disponibles dans le DOM à un moment donné, nous devons trouver un autre moyen de faire en sorte que la barre de défilement du navigateur reflète correctement la quantité de contenu théoriquement disponible. Nous allons utiliser un élément sentinelle de 1 x 1 px 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 sa propre couche pour nous assurer que la couche de la piste elle-même est complètement vide. Aucune couleur d'arrière-plan, rien. Si la couche de la piste n'est pas vide, elle n'est pas éligible aux optimisations du navigateur. Nous devrons donc stocker une texture sur notre carte graphique dont la hauteur est de quelques centaines de milliers de pixels. Cela n'est certainement pas viable sur un appareil mobile.
Chaque fois que nous faisons défiler la page, nous vérifions si le viewport est suffisamment proche de la fin de la piste. Si tel est le cas, nous étendrons la piste en déplaçant l'élément sentinelle et en déplaçant les éléments qui ont quitté la fenêtre d'affichage en bas de la piste, puis en les remplissant de nouveau contenu.
Il en va de même pour le défilement dans l'autre sens. Nous ne réduirons toutefois jamais la piste dans notre implémentation, afin que la position de la barre de défilement reste cohérente.
Tombstones
Comme nous l'avons mentionné précédemment, nous essayons de faire en sorte que notre source de données se comporte comme un élément du monde réel. Avec la latence du réseau et tout le reste. Cela signifie que si nos utilisateurs utilisent le défilement par balayage, ils peuvent facilement faire défiler la page au-delà du dernier élément pour lequel nous disposons de données. Dans ce cas, nous placerons un élément de type "tombstone" (espace réservé) qui sera remplacé par l'élément avec le contenu réel une fois que les données seront arrivées. Les pierres tombales sont également recyclées et disposent d'un pool distinct pour les éléments DOM réutilisables. Nous avons besoin de cela pour pouvoir effectuer une belle transition entre une pierre tombale et l'élément rempli de contenu, ce qui serait autrement très choquant pour l'utilisateur et pourrait même lui faire perdre le fil de ce sur quoi il se concentrait.

Un défi intéressant ici est que les éléments réels peuvent avoir une hauteur supérieure à celle de l'élément de type "tombstone" en raison de la différence de quantité 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 que des données seront reçues et qu'un indicateur de fin de vie sera remplacé au-dessus du viewport, en ancrant la position de défilement à un élément plutôt qu'à une valeur en pixels. Ce concept est appelé ancrage de défilement.
Ancrage du défilement
Notre ancrage de défilement sera appelé à la fois lorsque les pierres tombales sont remplacées et lorsque la fenêtre est redimensionnée (ce qui se produit également lorsque l'appareil est retourné). Nous devons déterminer l'élément le plus visible en haut de la fenêtre d'affichage. Comme cet élément ne peut être que partiellement visible, nous allons également stocker le décalage par rapport au haut de l'élément où commence la fenêtre d'affichage.

Si la fenêtre d'affichage est redimensionnée et que la piste a changé, nous pouvons restaurer une situation qui semble visuellement identique à l'utilisateur. Gagnez ! Sauf qu'une fenêtre redimensionnée signifie que la hauteur de chaque élément a potentiellement changé. Comment savoir à quelle hauteur le contenu ancré doit être placé ? Nous ne le faisons pas. 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 importante après un redimensionnement, ce que nous ne souhaitons pas. Nous partons plutôt du principe que chaque élément ci-dessus a la même taille qu'une pierre tombale et nous ajustons notre position de défilement en conséquence. Lorsque les éléments défilent dans la piste, nous ajustons notre position de défilement, ce qui reporte le travail de mise en page au moment où il est réellement nécessaire.
Mise en page
J'ai omis un détail important: la mise en page. Chaque recyclage d'un élément DOM entraînerait normalement une nouvelle mise en page de l'ensemble de la piste, ce qui nous placerait 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 semblant que tous les éléments plus loin sur la piste occupent toujours de l'espace, alors qu'en réalité, il n'y a que de l'espace vide. Comme nous effectuons nous-mêmes la mise en page, nous pouvons mettre en cache les positions où chaque élément se termine et nous pouvons charger immédiatement l'élément approprié à partir du cache lorsque l'utilisateur fait défiler l'écran vers l'arrière.
Idéalement, les éléments ne devraient être repeints qu'une seule fois lorsqu'ils sont associés au DOM et ne pas être affectés par l'ajout ou la suppression d'autres éléments dans la piste. C'est possible, mais uniquement avec les navigateurs modernes.
Ajustements de pointe
Récemment, Chrome a ajouté la prise en charge de la structuration CSS, une fonctionnalité qui permet aux développeurs d'indiquer au navigateur qu'un élément est une limite pour la mise en page et le travail de peinture. Comme nous effectuons nous-mêmes la mise en page, il s'agit d'une application idéale pour la structuration. Chaque fois que nous ajoutons un élément à la piste, nous savons que les autres éléments ne doivent pas être affectés par la nouvelle mise en page. Par conséquent, chaque élément doit obtenir contain: layout
. Nous ne voulons pas non plus affecter le reste de notre site Web. La piste elle-même doit donc également recevoir cette directive de style.
Nous avons également envisagé d'utiliser IntersectionObservers
comme mécanisme pour détecter quand l'utilisateur a fait défiler l'écran suffisamment loin pour que nous puissions commencer à recycler des éléments et à charger de nouvelles données. Toutefois, les IntersectionObservers sont spécifiés comme ayant une latence élevée (comme si vous utilisiez requestIdleCallback
). Nous pouvons donc ressentir une réactivité moindre avec les IntersectionObservers qu'avec les autres. Même notre implémentation actuelle utilisant l'événement scroll
souffre de ce problème, car les événements de défilement sont distribués de manière "meilleure effort". À terme, le worklet de composition de Houdini sera la solution haute fidélité à ce problème.
Ce n'est toujours pas parfait
Notre implémentation actuelle du recyclage du DOM n'est pas idéale, car elle ajoute tous les éléments qui passent par le viewport, au lieu de se soucier uniquement de ceux qui sont à l'écran. Cela signifie que lorsque vous faites défiler l'écran vraiment rapidement, vous demandez tellement de travail à Chrome pour la mise en page et la peinture qu'il ne peut pas suivre. Vous ne verrez plus que l'arrière-plan. Ce n'est pas la fin du monde, mais il faut certainement améliorer ce point.
Nous espérons que vous avez compris à quel point les problèmes simples peuvent devenir difficiles lorsque vous souhaitez combiner une expérience utilisateur de qualité avec des normes de performances élevées. Les progressive web apps devenant des expériences de base sur les téléphones mobiles, cela deviendra plus important, et les développeurs Web devront continuer à investir dans l'utilisation de modèles qui respectent les contraintes de performances.
Vous trouverez tout le code dans notre dépôt. Nous avons fait de notre mieux pour le rendre réutilisable, mais nous ne le publierons pas en tant que bibliothèque sur npm ni en tant que dépôt distinct. L'utilisation principale est pédagogique.