En bref : réutilisez vos éléments DOM et supprimez ceux qui sont loin 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.
Les défilements infinis sont partout sur Internet. La liste des artistes de Google Music, la timeline de Facebook et le flux en direct de Twitter sont des exemples de ces listes. Vous faites défiler la page vers le bas et, avant d'atteindre la fin, de nouveaux contenus apparaissent comme par magie. L'expérience est fluide pour les utilisateurs, et il est facile de comprendre pourquoi.
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 ce qu'il faut™ est vaste. Cela commence par des choses simples, comme les liens dans le pied de page qui deviennent pratiquement inaccessibles, car le contenu continue de repousser le pied de page. Mais les problèmes se compliquent. Comment gérez-vous un événement de redimensionnement lorsqu'un utilisateur fait passer son téléphone du mode Portrait au mode Paysage ? Comment empêchez-vous votre téléphone de s'arrêter brusquement lorsque la liste devient trop longue ?
The right thing™
Nous avons pensé que c'était une raison suffisante 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 maintenant des normes de performances.
Pour atteindre notre objectif, nous allons utiliser trois techniques : le recyclage du DOM, les "tombstones" et l'ancrage de défilement.
Notre cas de démonstration sera une fenêtre de chat de type 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 scrollers infinis n'est vraiment infini, mais avec la quantité de données disponibles pour alimenter ces scrollers, ils pourraient tout aussi bien l'être. Pour plus de simplicité, nous allons simplement coder en dur un ensemble de messages de chat et choisir un message, un auteur et une pièce jointe d'image occasionnelle au hasard, avec un peu de délai 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 maintenir un faible 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 de nouveaux. Certes, les nœuds DOM eux-mêmes ne sont pas chers, mais ils ne sont pas sans frais, car chacun d'eux ajoute un coût supplémentaire en termes de mémoire, de mise en page, de style et de peinture. Les appareils bas de gamme deviendront sensiblement plus lents, voire complètement inutilisables, si le site Web comporte un DOM trop volumineux à gérer. Gardez également à l'esprit que chaque réorganisation et réapplication de vos styles (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 grand. Le recyclage de vos nœuds DOM signifie que nous allons maintenir le nombre total de nœuds DOM considérablement plus bas, 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 petit sous-ensemble 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 présente. Nous allons utiliser un élément sentinelle de 1 x 1 px avec une transformation pour forcer l'élément qui contient les éléments (la piste) à avoir la hauteur souhaitée. Nous allons promouvoir chaque élément de la piste dans son propre calque pour nous assurer que le calque de la piste lui-même est complètement vide. Aucune couleur d'arrière-plan, rien. Si le calque de la piste n'est pas vide, il n'est pas éligible aux optimisations du navigateur et nous devrons stocker sur notre carte graphique une texture dont la hauteur est de plusieurs centaines de milliers de pixels. Elle n'est certainement pas viable sur un appareil mobile.
Chaque fois que nous faisons défiler la page, nous vérifions si la fenêtre d'affichage s'est suffisamment rapprochée de la fin de la piste. Si c'est le cas, nous étendrons la piste en déplaçant l'élément sentinelle et les éléments qui ont quitté la fenêtre d'affichage vers le bas de la piste, puis nous les remplirons avec de nouveaux contenus.
Il en va de même pour le défilement dans l'autre sens. Toutefois, nous ne réduirons jamais la marge dans notre implémentation, de sorte 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 quelque chose dans le monde réel. Avec la latence du réseau et tout le reste. Cela signifie que si nos utilisateurs utilisent le défilement rapide, 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 "tombstone" (un espace réservé) qui sera remplacé par l'élément contenant le contenu réel une fois les données reçues. Les pierres tombales sont également recyclées et disposent d'un pool distinct pour les éléments DOM réutilisables. Nous en avons besoin pour effectuer une transition fluide entre un emplacement vide et l'élément rempli de contenu. Sans cela, l'utilisateur pourrait être déstabilisé et perdre de vue ce sur quoi il se concentrait.
Un défi intéressant ici est que les éléments réels peuvent avoir une hauteur plus importante que l'élément factice en raison de la 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 arriveront et qu'une pierre tombale sera remplacée au-dessus de la fenêtre d'affichage, 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 invoqué à la fois lorsque les espaces réservés seront remplacés et lorsque la fenêtre sera redimensionnée (ce qui se produit également lorsque l'appareil est retourné). Nous devrons 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 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. Gagné ! Cependant, une fenêtre redimensionnée signifie que la hauteur de chaque élément a potentiellement changé. Comment savoir à quelle distance le contenu ancré doit être placé ? Non. Pour le savoir, nous devrions disposer 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. Au lieu de cela, nous partons du principe que chaque élément ci-dessus a la même taille qu'une pierre tombale et ajustons notre position de défilement en conséquence. Lorsque des éléments sont déplacés dans la piste, nous ajustons notre position de défilement, ce qui permet de différer le travail de mise en page jusqu'à ce qu'il soit réellement nécessaire.
Disposition
J'ai oublié un détail important : la mise en page. Chaque recyclage d'un élément DOM relancerait normalement la mise en page de l'ensemble de la piste, ce qui nous ferait passer bien en dessous de notre objectif de 60 images par seconde. Pour éviter cela, nous prenons en charge 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 plus haut sur la piste occupaient encore de l'espace alors qu'il n'y a en réalité que de l'espace vide. Comme nous effectuons la mise en page nous-mêmes, nous pouvons mettre en cache les positions où chaque élément se termine et nous pouvons charger immédiatement l'élément correct à partir du cache lorsque l'utilisateur fait défiler la page vers le haut.
Idéalement, les éléments ne seraient repeints qu'une seule fois lorsqu'ils sont associés au DOM et ne seraient pas affectés par l'ajout ou la suppression d'autres éléments dans la piste. C'est possible, mais uniquement avec les navigateurs récents.
Ajustements de pointe
Récemment, Chrome a ajouté la prise en charge de l'isolement CSS, une fonctionnalité qui permet aux développeurs de dire au navigateur qu'un élément est une limite pour la mise en page et la peinture. Comme nous gérons nous-mêmes la mise en page ici, il s'agit d'une application idéale 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 nouvelle mise en page. Chaque élément doit donc 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 la page 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 étant à latence élevée (comme si requestIdleCallback était utilisé). Nous pouvons donc en fait ressentir une réactivité moindre avec les IntersectionObservers que sans. 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 sur la base du "meilleur effort". À terme, le composant Worklet de Houdini devrait être la solution haute fidélité à ce problème.
Il n'est pas encore parfait
Notre implémentation actuelle du recyclage du DOM n'est pas idéale, car elle ajoute tous les éléments qui passent par la fenêtre d'affichage, au lieu de se soucier uniquement de ceux qui sont réellement à l'écran. Cela signifie que lorsque vous faites défiler la page trèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèèè Vous ne verrez que l'arrière-plan. Ce n'est pas la fin du monde, mais c'est un point à améliorer.
Nous espérons que vous comprenez à quel point des problèmes simples peuvent devenir complexes lorsque vous souhaitez combiner une excellente expérience utilisateur 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 qu'il soit 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.