Maintenant, vous me voyez : comment différer, charger paresseux et agir avec IntersectionObserver

Publié: 2022-03-10
Résumé rapide ↬ Les informations sur les intersections sont nécessaires pour de nombreuses raisons, telles que le chargement paresseux des images. Mais il y a tellement plus. Il est temps d'avoir une meilleure compréhension et des perspectives différentes sur l'API Intersection Observer. Prêt?

Il était une fois un développeur Web qui a réussi à convaincre ses clients que les sites ne devaient pas se ressembler dans tous les navigateurs, se souciait de l'accessibilité et a été l'un des premiers à adopter les grilles CSS. Mais au fond de son cœur, c'était la performance qui était sa véritable passion : il optimisait, minifiait, surveillait et même utilisait des astuces psychologiques en permanence dans ses projets.

Puis, un jour, il a appris le chargement différé d'images et d'autres éléments qui ne sont pas immédiatement visibles pour les utilisateurs et ne sont pas essentiels pour afficher un contenu significatif à l'écran. C'était le début de l'aube : le développeur est entré dans le monde diabolique des plugins jQuery à chargement paresseux (ou peut-être le monde pas si diabolique des attributs async et defer ). Certains disent même qu'il est entré directement au cœur de tous les maux : le monde des auditeurs d'événements de scroll . Nous ne saurons jamais avec certitude où il s'est retrouvé, mais encore une fois, ce développeur est absolument fictif, et toute similitude avec un développeur n'est qu'une coïncidence.

un développeur web
Le développeur web fictif

Eh bien, vous pouvez maintenant dire que la boîte de Pandore a été ouverte et que notre développeur fictif ne rend pas le problème moins réel. De nos jours, donner la priorité au contenu au-dessus de la ligne de flottaison est devenu extrêmement important pour la performance de nos projets Web, tant du point de vue de la vitesse que du poids des pages.

Plus après saut! Continuez à lire ci-dessous ↓

Dans cet article, nous allons sortir de l'obscurité du scroll et parler de la manière moderne de charger les ressources paresseusement. Pas seulement le chargement paresseux d'images, mais le chargement de n'importe quel élément d'ailleurs. Plus encore, la technique dont nous allons parler aujourd'hui est capable de bien plus que le simple chargement paresseux d'actifs : nous serons en mesure de fournir tout type de fonctionnalité différée en fonction de la visibilité des éléments pour les utilisateurs.

IntersectionObserver : Maintenant, tu me vois

Mesdames et messieurs, parlons de l'API Intersection Observer. Mais avant de commencer, jetons un coup d'œil au paysage des outils modernes qui nous a conduit à IntersectionObserver .

2017 a été une très bonne année pour les outils intégrés à nos navigateurs, nous aidant à améliorer la qualité ainsi que le style de notre base de code sans trop d'efforts. Ces jours-ci, le Web semble s'éloigner des solutions sporadiques basées sur des solutions très différentes pour résoudre des problèmes très typiques vers une approche plus bien définie des interfaces Observer (ou simplement des «observateurs»): MutationObserver bien pris en charge a obtenu de nouveaux membres de la famille qui ont été rapidement adopté dans les navigateurs modernes :

  • IntersectionObservateur et
  • PerformanceObserver (dans le cadre de la spécification Performance Timeline Level 2).

Un autre membre potentiel de la famille, FetchObserver, est un travail en cours et nous guide davantage dans les terres d'un proxy réseau, mais aujourd'hui, je voudrais plutôt parler davantage du front-end.

IntersectionObserver et PerformanceObserver sont les nouveaux membres de la famille Observers.
IntersectionObserver et PerformanceObserver sont les nouveaux membres de la famille Observers.

PerformanceObserver et IntersectionObserver visent à aider les développeurs front-end à améliorer les performances de leurs projets à différents moments. Le premier nous donne l'outil pour le Real User Monitoring, tandis que le second est l'outil, nous fournissant une amélioration tangible des performances. Comme mentionné précédemment, cet article examinera en détail exactement ce dernier : IntersectionObserver . Afin de comprendre les mécanismes d' IntersectionObserver en particulier, nous devrions examiner comment un observateur générique est censé fonctionner dans le Web moderne.

Conseil de pro : vous pouvez ignorer la théorie et plonger tout de suite dans les mécanismes d'IntersectionObserver ou, même plus loin, directement dans les applications possibles d' IntersectionObserver .

Observateur contre événement

Un « observateur », comme son nom l'indique, est destiné à observer quelque chose qui se passe dans le contexte d'une page. Les observateurs peuvent observer quelque chose qui se passe sur une page, comme les changements DOM. Ils peuvent également surveiller les événements du cycle de vie de la page. Les observateurs peuvent également exécuter certaines fonctions de rappel. Maintenant, un lecteur attentif pourrait immédiatement repérer le problème ici et demander : « Alors, à quoi ça sert ? N'avons-nous pas déjà des événements à cet effet ? Qu'est-ce qui différencie les Observateurs ? » Très bon point ! Regardons de plus près et trions cela.

Observateur vs événement : quelle est la différence ?
Observateur contre événement : quelle est la différence ?

La différence cruciale entre l'événement normal et l'observateur est que, par défaut, le premier réagit de manière synchrone à chaque occurrence de l'événement, affectant la réactivité du thread principal, tandis que le second doit réagir de manière asynchrone sans trop affecter les performances. Au moins, cela est vrai pour les observateurs actuellement présentés : tous se comportent de manière asynchrone , et je ne pense pas que cela changera à l'avenir.

Cela conduit à la principale différence dans la gestion des rappels des observateurs qui pourrait dérouter les débutants : la nature asynchrone des observateurs peut entraîner la transmission simultanée de plusieurs observables à une fonction de rappel. Pour cette raison, la fonction de rappel ne doit pas s'attendre à une seule entrée mais à un Array d'entrées (même si parfois le tableau ne contiendra qu'une seule entrée).

De plus, certains observateurs (en particulier celui dont nous parlons aujourd'hui) fournissent des propriétés pré-calculées très pratiques, que nous utilisions autrement pour calculer nous-mêmes en utilisant des méthodes et des propriétés coûteuses (du point de vue des performances) lors de l'utilisation d'événements réguliers. Pour clarifier ce point, nous verrons un exemple un peu plus loin dans l'article.

Donc, s'il est difficile pour quelqu'un de s'éloigner du paradigme de l'événement, je dirais que les observateurs sont des événements sous stéroïdes. Une autre description serait : Les observateurs sont un nouveau niveau d'approximation en plus des événements. Mais quelle que soit la définition que vous préférez, il va sans dire que les Observateurs ne sont pas destinés à remplacer les événements (du moins pas encore) ; il y a suffisamment de cas d'utilisation pour les deux, et ils peuvent vivre côte à côte avec bonheur.

Les observateurs ne sont pas destinés à remplacer les événements : les deux peuvent vivre ensemble heureux.
Les observateurs ne sont pas destinés à remplacer les événements : les deux peuvent vivre ensemble heureux.

Structure générique de l'observateur

La structure générique d'un observateur (n'importe lequel de ceux disponibles au moment de la rédaction) ressemble à ceci :

 /** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);

Encore une fois, notez que entries sont un Array de valeurs, pas une seule entrée.

Voici la structure générique : les implémentations d'observateurs particuliers diffèrent par les arguments passés dans son observe() et les arguments passés dans son rappel. Par exemple, MutationObserver devrait également obtenir un objet de configuration pour en savoir plus sur les changements à observer dans le DOM. PerformanceObserver n'observe pas les nœuds dans DOM, mais dispose à la place d'un ensemble dédié de types d'entrées qu'il peut observer.

Ici, terminons la partie "générique" de cette discussion et approfondissons le sujet de l'article d'aujourd'hui - IntersectionObserver .

Déconstruire IntersectionObserver

Déconstruire IntersectionObserver
Déconstruire IntersectionObserver

Tout d'abord, définissons ce qu'est IntersectionObserver .

Selon MDN :

L'API Intersection Observer permet d'observer de manière asynchrone les changements dans l'intersection d'un élément cible avec un élément ancêtre ou avec la fenêtre d'affichage d'un document de niveau supérieur.

En termes simples, IntersectionObserver observe de manière asynchrone le chevauchement d'un élément par un autre élément. Parlons de ce que sont ces éléments dans IntersectionObserver .

Initialisation d'IntersectionObserver

Dans l'un des paragraphes précédents, nous avons vu la structure d'un Observateur générique. IntersectionObserver étend un peu cette structure. Tout d'abord, ce type d'Observer nécessite une configuration avec trois éléments principaux :

  • root : C'est l'élément racine utilisé pour l'observation. Il définit le « cadre de capture » de base pour les éléments observables. Par défaut, la root est la fenêtre de votre navigateur mais peut vraiment être n'importe quel élément de votre DOM (vous définissez alors la root sur quelque chose comme document.getElementById('your-element') ). Gardez cependant à l'esprit que les éléments que vous souhaitez observer doivent "vivre" dans l'arborescence DOM de root dans ce cas.
propriété root de la configuration d'IntersectionObserver
La propriété root définit la base pour 'capturer le cadre' pour nos éléments.
  • rootMargin : définit la marge autour de votre élément root qui étend ou rétrécit le "cadre de capture" lorsque les dimensions de votre root ne fournissent pas suffisamment de flexibilité. Les options pour les valeurs de cette configuration sont similaires à celles de margin en CSS, telles que rootMargin: '50px 20px 10px 40px' (top, right bottom, left). Les valeurs peuvent être abrégées (comme rootMargin: '50px' ) et peuvent être exprimées en px ou % . Par défaut, rootMargin: '0px' .
propriété rootMargin de la configuration d'IntersectionObserver
La propriété rootMargin développe/contracte le "cadre de capture" qui est défini par root .
  • threshold : il n'est pas toujours souhaité de réagir instantanément lorsqu'un élément observé croise une bordure du "cadre de capture" (défini comme une combinaison de root et rootMargin ). Le threshold définit le pourcentage d'une telle intersection auquel l'observateur doit réagir. Il peut être défini comme une valeur unique ou comme un tableau de valeurs. Pour mieux comprendre l'effet du threshold (je sais que cela peut parfois prêter à confusion), voici quelques exemples :
    • threshold: 0 : La valeur par défaut IntersectionObserver doit réagir lorsque le tout premier ou le tout dernier pixel d'un élément observé croise l'une des bordures du « cadre de capture ». Gardez à l'esprit IntersectionObserver est indépendant de la direction, ce qui signifie qu'il réagira dans les deux scénarios : a) lorsque l'élément entre et b) lorsqu'il quitte le "cadre de capture".
    • threshold: 0.5 : l'observateur doit être déclenché lorsque 50 % d'un élément observé coupe le "cadre de capture" ;
    • threshold: [0, 0.2, 0.5, 1] ​​: L'observateur doit réagir dans 4 cas :
      • Le tout premier pixel d'un élément observé entre dans le « cadre de capture » : l'élément n'est toujours pas vraiment dans ce cadre, ou le tout dernier pixel de l'élément observé sort du « cadre de capture » : l'élément n'est plus dans le cadre ;
      • 20 % de l'élément se trouve dans le "cadre de capture" (là encore, la direction n'a pas d'importance pour IntersectionObserver );
      • 50 % de l'élément se trouve dans le "cadre de capture" ;
      • 100 % de l'élément se trouve dans le "cadre de capture". Ceci est strictement opposé au threshold: 0 .
propriété de seuil de la configuration d'IntersectionObserver
La propriété de threshold définit de combien l'élément doit croiser notre 'cadre de capture' avant que l'Observateur ne soit déclenché.

Afin d'informer notre IntersectionObserver de notre configuration souhaitée, nous passons simplement notre objet de config dans le constructeur de notre Observer avec notre fonction de rappel comme ceci :

 const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);

Maintenant, nous devons donner à IntersectionObserver l'élément réel à observer. Cela se fait simplement en passant l'élément à la fonction observe() :

 … const img = document.getElementById('image-to-observe'); observer.observe(image);

Quelques points à noter à propos de cet élément observé :

  • Cela a été mentionné précédemment, mais mérite d'être mentionné à nouveau : si vous définissez root comme élément dans le DOM, l'élément observé doit être situé dans l'arborescence DOM de root .
  • IntersectionObserver ne peut accepter qu'un seul élément d'observation à la fois et ne prend pas en charge l'approvisionnement par lots pour les observations. Cela signifie que si vous avez besoin d'observer plusieurs éléments (disons plusieurs images sur une page), vous devez parcourir chacun d'eux et observer chacun d'eux séparément :
 … const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
  • Lors du chargement d'une page avec Observer en place, vous remarquerez peut-être que le rappel de IntersectionObserver a été déclenché pour tous les éléments observés à la fois. Même ceux qui ne correspondent pas à la configuration fournie. "Eh bien… pas vraiment ce à quoi je m'attendais", est la pensée habituelle lorsque l'on en fait l'expérience pour la première fois. Mais ne vous y trompez pas : cela ne signifie pas nécessairement que ces éléments observés croisent d'une manière ou d'une autre le « cadre de capture » pendant le chargement de la page.
Capture d'écran de DevTools avec IntersectionObserver déclenché pour tous les éléments à la fois.
IntersectionObserver sera déclenché pour tous les éléments observés une fois qu'ils seront enregistrés, mais cela ne signifie pas qu'ils intersectent tous notre «cadre de capture».

Cela signifie cependant que l'entrée de cet élément a été initialisée et est maintenant contrôlée par votre IntersectionObserver . Cependant, cela peut ajouter du bruit inutile à votre fonction de rappel, et il devient de votre responsabilité de détecter quels éléments croisent effectivement le "cadre de capture" et dont nous n'avons toujours pas besoin de tenir compte. Pour comprendre comment effectuer cette détection, approfondissons un peu l'anatomie de notre fonction de rappel et examinons en quoi consistent ces entrées.

IntersectionObserver Rappel

Tout d'abord, la fonction de rappel pour un IntersectionObserver prend deux arguments, et nous en parlerons dans l'ordre inverse en commençant par le deuxième argument. Avec le Array d'entrées observées susmentionné, croisant notre "cadre de capture", la fonction de rappel obtient l' observateur lui-même comme deuxième argument.

Référence à l'observateur lui-même

 new IntersectionObserver(function(entries, SELF) {…});

Obtenir la référence à l'Observer lui-même est utile dans de nombreux scénarios lorsque vous souhaitez arrêter d'observer un élément après qu'il a été détecté par l' IntersectionObserver pour la première fois. Des scénarios tels que le chargement paresseux des images, la récupération différée d'autres actifs, etc. sont de ce type. Lorsque vous souhaitez arrêter d'observer un élément, IntersectionObserver fournit une méthode unobserve(element-to-stop-observing) qui peut être exécutée dans la fonction de rappel après avoir effectué certaines actions sur l'élément observé (comme le chargement paresseux réel d'une image, par exemple ).

Certains de ces scénarios seront examinés plus loin dans l'article, mais avec ce deuxième argument hors de notre chemin, passons aux principaux acteurs de ce jeu de rappel.

IntersectionObserverEntry

 new IntersectionObserver(function(ENTRIES, self) {…});

Les entries que nous obtenons dans notre fonction de rappel en tant que Array sont du type spécial : IntersectionObserverEntry . Cette interface nous fournit un ensemble prédéfini et pré-calculé de propriétés concernant chaque élément particulier observé. Jetons un coup d'œil aux plus intéressants.

Tout d'abord, les entrées de type IntersectionObserverEntry des informations sur trois rectangles différents - définissant les coordonnées et les limites des éléments impliqués dans le processus :

  • rootBounds : un rectangle pour le "cadre de capture" ( root + rootMargin );
  • boundingClientRect : un rectangle pour l'élément observé lui-même ;
  • intersectionRect : une zone du "cadre de capture" intersectée par l'élément observé.
Rectangles d'intersectionObserverEntry
Tous les rectangles englobants impliqués dans IntersectionObserverEntry sont calculés pour vous.

La chose vraiment cool à propos de ces rectangles calculés pour nous de manière asynchrone est qu'elle nous donne des informations importantes liées au positionnement de l'élément sans que nous getBoundingClientRect() , offsetTop , offsetLeft et d'autres propriétés et méthodes de positionnement coûteuses déclenchant le thrashing de la mise en page. Pure victoire pour la performance !

Une autre propriété de l'interface IntersectionObserverEntry qui nous intéresse est isIntersecting . Il s'agit d'une propriété pratique indiquant si l'élément observé croise actuellement le "cadre de capture" ou non. Nous pourrions, bien sûr, obtenir cette information en regardant l' intersectionRect (si ce rectangle n'est pas 0 × 0, l'élément croise le "cadre de capture") mais avoir cela pré-calculé pour nous est assez pratique.

isIntersecting peut être utilisé pour savoir si l'élément observé vient juste d'entrer dans le "cadre de capture" ou s'il en sort déjà. Pour le savoir, enregistrez la valeur de cette propriété en tant qu'indicateur global et lorsque la nouvelle entrée pour cet élément arrive à votre fonction de rappel, comparez sa nouvelle isIntersecting avec cet indicateur global :

  • Si c'était false et maintenant c'est true , alors l'élément entre dans le « cadre de capture » ;
  • Si c'est le contraire et que c'est false maintenant alors que c'était true avant, alors l'élément quitte le "cadre de capture".

isIntersecting est exactement la propriété qui nous aide à résoudre le problème dont nous avons parlé plus tôt, c'est-à-dire séparer les entrées pour les éléments qui croisent réellement le « cadre de capture » du bruit de ceux qui ne sont que l'initialisation de l'entrée.

 let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);

REMARQUE : Dans Microsoft Edge 15, la propriété isIntersecting n'a pas été implémentée, renvoyant undefined malgré la prise en charge complète d' IntersectionObserver autrement. Cela a été corrigé en juillet 2017 et est disponible depuis Edge 16.

L'interface IntersectionObserverEntry fournit une autre propriété pratique pré-calculée : intersectionRatio . Ce paramètre peut être utilisé aux mêmes fins que isIntersecting mais fournit un contrôle plus granulaire car il s'agit d'un nombre à virgule flottante au lieu d'une valeur booléenne. La valeur de intersectionRatio indique la proportion de la zone de l'élément observé qui croise le « cadre de capture » (le rapport entre la zone intersectionRect et la zone boundingClientRect ). Encore une fois, nous pourrions faire ce calcul nous-mêmes en utilisant les informations de ces rectangles, mais c'est bien de le faire pour nous.

Cela ne vous semble-t-il pas déjà familier ? Oui, la propriété <code>intersectionRatio</code> est similaire à la propriété <code>threshold</code> de la configuration d'Observer. La différence est que ce dernier définit <em>quand</em> lancer Observer, le premier indique la situation réelle de l'intersection (qui est légèrement différente du <code>seuil</code> en raison de la nature asynchrone d'Observer).
Cela ne vous semble-t-il pas déjà familier ? Oui, la propriété intersectionRatio est similaire à la propriété threshold de la configuration d'Observer. La différence est que ce dernier définit * quand * lancer Observer, le premier indique la situation réelle de l'intersection (qui est légèrement différente du threshold en raison de la nature asynchrone d'Observer).

target est une autre propriété de l'interface IntersectionObserverEntry à laquelle vous devrez peut-être accéder assez souvent. Mais il n'y a absolument aucune magie ici - c'est juste l'élément d'origine qui a été passé à la fonction observe() de votre Observer. Tout comme event.target vous êtes habitué lorsque vous travaillez avec des événements.

Pour obtenir la liste complète des propriétés de l'interface IntersectionObserverEntry , vérifiez la spécification.

Applications possibles

Je me rends compte que vous êtes probablement arrivé à cet article exactement à cause de ce chapitre : qui se soucie de la mécanique quand nous avons des extraits de code à copier-coller après tout ? Alors ne vous embêtez pas avec plus de discussion maintenant : nous entrons dans le pays du code et des exemples. J'espère que les commentaires inclus dans le code rendront les choses plus claires.

Fonctionnalité différée

Tout d'abord, passons en revue un exemple révélant les principes de base qui sous-tendent l'idée d' IntersectionObserver . Disons que vous avez un élément qui doit faire beaucoup de calculs une fois qu'il est à l'écran. Par exemple, votre annonce ne doit enregistrer une vue que lorsqu'elle a effectivement été diffusée auprès d'un utilisateur. Mais maintenant, imaginons que vous ayez un élément de carrousel à lecture automatique quelque part sous le premier écran de votre page.

Carrousel sous le premier écran de votre application
Lorsque nous avons un carrousel ou toute autre fonctionnalité lourde sous le pli de notre application, c'est un gaspillage de ressources de commencer à l'amorcer/le charger immédiatement.

Faire fonctionner un carrousel, en général, est une lourde tâche. Habituellement, cela implique des minuteries JavaScript, des calculs pour faire défiler automatiquement les éléments, etc. Toutes ces tâches chargent le thread principal, et lorsqu'elles sont effectuées en mode de lecture automatique, il nous est difficile de savoir quand notre thread principal obtient ce hit. Lorsque nous parlons de prioriser le contenu sur notre premier écran et que nous voulons frapper First Meaningful Paint et Time To Interactive dès que possible, le fil principal bloqué devient un goulot d'étranglement pour nos performances.

Pour résoudre le problème, nous pouvons différer la lecture d'un tel carrousel jusqu'à ce qu'il entre dans la fenêtre d'affichage du navigateur. Pour ce cas, nous utiliserons nos connaissances et notre exemple pour le paramètre isIntersecting de l'interface IntersectionObserverEntry .

 const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);

Ici, nous jouons le carrousel uniquement lorsqu'il entre dans notre fenêtre d'affichage. Notez l'absence d'objet de config passé à l'initialisation d' IntersectionObserver : cela signifie que nous nous appuyons sur les options de configuration par défaut. Lorsque le carrousel sort de notre fenêtre, nous devons arrêter de le jouer pour ne pas dépenser de ressources sur les éléments qui ne sont plus importants.

Chargement paresseux des actifs

C'est probablement le cas d'utilisation le plus évident pour IntersectionObserver : nous ne voulons pas dépenser de ressources pour télécharger quelque chose dont l'utilisateur n'a pas besoin en ce moment. Cela apportera un énorme avantage à vos utilisateurs : les utilisateurs n'auront pas besoin de télécharger et leurs appareils mobiles n'auront pas besoin d'analyser et de compiler de nombreuses informations inutiles dont ils n'ont pas besoin pour le moment. Sans surprise du tout, cela contribuera également aux performances de votre application.

Images à chargement paresseux sous le pli
Chargement paresseux d'actifs tels que des images situées sous le premier écran - l'application la plus évidente d'IntersectionObserver.

Auparavant, afin de différer le téléchargement et le traitement des ressources jusqu'au moment où l'utilisateur pouvait les afficher à l'écran, nous avions affaire à des écouteurs d'événements sur des événements tels que scroll . Le problème est évident : cela a trop souvent déclenché les auditeurs. Nous avons donc dû trouver l'idée d'étrangler ou de faire rebondir l'exécution du rappel. Mais tout cela a ajouté beaucoup de pression sur notre fil principal en le bloquant au moment où nous en avions le plus besoin.

Donc, pour revenir à IntersectionObserver dans un scénario de chargement paresseux, sur quoi devrions-nous garder un œil ? Examinons un exemple simple d'images à chargement différé.

Voir le chargement Pen Lazy dans IntersectionObserver par Denys Mishunov (@mishunov) sur CodePen.

Voir le chargement Pen Lazy dans IntersectionObserver par Denys Mishunov (@mishunov) sur CodePen.

Essayez de faire défiler lentement cette page vers le "troisième écran" et regardez la fenêtre de surveillance dans le coin supérieur droit : elle vous indiquera combien d'images ont été téléchargées jusqu'à présent.

Au cœur du balisage HTML pour cette tâche se trouve une simple séquence d'images :

 … <img data-src="https://blah-blah.com/foo.jpg"> …

Comme vous pouvez le voir, les images doivent être livrées sans balises src : une fois qu'un navigateur voit l'attribut src , il commencera immédiatement à télécharger cette image, ce qui est contraire à nos intentions. Par conséquent, nous ne devrions pas mettre cet attribut sur nos images en HTML, et à la place, nous pourrions nous fier à un attribut de data- comme data-src ici.

Une autre partie de cette solution est, bien sûr, JavaScript. Concentrons-nous ici sur les éléments principaux :

 const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });

Du point de vue de la structure, il n'y a rien de nouveau ici : nous avons déjà couvert tout cela :

  • Nous recevons tous les messages avec nos attributs data-src ;
  • Set config : pour ce scénario, vous souhaitez étendre votre "cadre de capture" pour détecter des éléments un peu plus bas que le bas de la fenêtre ;
  • Enregistrez IntersectionObserver avec cette configuration ;
  • Itérez sur nos images et ajoutez-les toutes pour qu'elles soient observées par cet IntersectionObserver ;

La partie intéressante se produit dans la fonction de rappel invoquée sur les entrées. Il y a trois étapes essentielles impliquées.

  1. Tout d'abord, nous ne traitons que les éléments qui croisent réellement notre "cadre de capture". Cet extrait devrait vous être familier maintenant.

     entries.forEach(entry => { if (entry.isIntersecting) { … } });

  2. Ensuite, nous traitons en quelque sorte l'entrée en convertissant notre image avec data-src en un vrai <img src="…"> .

     if (entry.isIntersecting) { preloadImage(entry.target); … }
    Cela déclenchera le navigateur pour enfin télécharger l'image. preloadImage() est une fonction très simple qui ne mérite pas d'être mentionnée ici. Il suffit de lire la source.

  3. Prochaine et dernière étape : étant donné que le chargement paresseux est une action unique et que nous n'avons pas besoin de télécharger l'image chaque fois que l'élément entre dans notre "cadre de capture", nous devons unobserve l'image déjà traitée. De la même manière que nous devrions le faire avec element.removeEventListener() pour nos événements réguliers lorsque ceux-ci ne sont plus nécessaires pour éviter les fuites de mémoire dans notre code.

     if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as self to our callback self.unobserve(entry.target); }

Noter. Au lieu de unobserve(event.target) nous pourrions aussi appeler disconnect() : cela déconnecte complètement notre IntersectionObserver et n'observerait plus les images. Ceci est utile si la seule chose qui vous intéresse est le premier coup de votre Observateur. Dans notre cas, nous avons besoin de l'observateur pour continuer à surveiller les images, nous ne devrions donc pas nous déconnecter pour l'instant.

N'hésitez pas à bifurquer l'exemple et à jouer avec différents paramètres et options. Il y a cependant une chose intéressante à mentionner lorsque vous souhaitez charger paresseusement les images en particulier. Vous devez toujours garder à l'esprit la boîte générée par l'élément observé ! Si vous vérifiez l'exemple, vous remarquerez que le CSS pour les images des lignes 41 à 47 contient des styles supposés redondants, incl. min-height: 100px . Ceci est fait pour donner aux espaces réservés de l'image ( <img> sans l'attribut src ) une certaine dimension verticale. Pourquoi?

  • Sans dimensions verticales, toutes les balises <img> généreraient une boîte 0×0 ;
  • Étant donné que la <img> génère une sorte de boîte de inline-block par défaut, toutes ces boîtes 0 × 0 seraient alignées côte à côte sur la même ligne ;
  • Cela signifie que votre IntersectionObserver enregistrera toutes (ou, selon la vitesse à laquelle vous faites défiler, presque toutes) les images à la fois - probablement pas tout à fait ce que vous voulez réaliser.

Mise en surbrillance de la section actuelle

IntersectionObserver est bien plus qu'un simple chargement paresseux, bien sûr. Voici un autre exemple de remplacement d'événement de scroll par cette technologie. Dans celui-ci, nous avons un scénario assez courant : sur la barre de navigation fixe, nous devons mettre en surbrillance la section actuelle en fonction de la position de défilement du document.

Voir la section actuelle Pen Highlighting dans IntersectionObserver par Denys Mishunov (@mishunov) sur CodePen.

Voir la section actuelle Pen Highlighting dans IntersectionObserver par Denys Mishunov (@mishunov) sur CodePen.

Structurellement, il est similaire à l'exemple pour les images à chargement différé et a la même structure de base avec les exceptions suivantes :

  • Maintenant, nous voulons observer non pas les images, mais les sections de la page ;
  • De toute évidence, nous avons également une fonction différente pour traiter les entrées de notre rappel ( intersectionHandler(entry) ). Mais celui-ci n'est pas intéressant : il ne fait que basculer la classe CSS.

Ce qui est intéressant ici, c'est l'objet de config :

 const config = { rootMargin: '-50px 0px -55% 0px' };

Pourquoi pas la valeur par défaut de 0px pour rootMargin , demandez-vous ? Eh bien, tout simplement parce que la mise en évidence de la section actuelle et le chargement paresseux d'une image sont très différents dans ce que nous essayons de réaliser. Avec le chargement paresseux, nous voulons commencer le chargement avant que l'image n'entre dans la vue. C'est pourquoi, à cette fin, nous avons étendu notre "cadre de capture" de 50px en bas. Au contraire, lorsque nous voulons mettre en évidence la section en cours, nous devons être sûrs que la section est réellement visible à l'écran. Et pas seulement cela : nous devons être sûrs que l'utilisateur lit ou va lire exactement cette section. Par conséquent, nous voulons qu'une section aille un peu plus de la moitié de la fenêtre à partir du bas avant de pouvoir la déclarer section active. De plus, nous voulons tenir compte de la hauteur de la barre de navigation, et donc nous supprimons la hauteur de la barre du "cadre de capture".

Capture d'image pour la section actuelle
Nous voulons que l'Observer détecte uniquement les éléments qui entrent dans le "cadre de capture" entre 50px du haut et 55% de la fenêtre d'affichage du bas.

Notez également qu'en cas de mise en surbrillance de l'élément de navigation actuel, nous ne voulons pas arrêter d'observer quoi que ce soit. Ici, nous devons toujours garder IntersectionObserver en charge, vous ne trouverez donc ni disconnect() ni unobserve() ici.

Sommaire

IntersectionObserver est une technologie très simple. Il a un assez bon support dans les navigateurs modernes et si vous voulez l'implémenter pour les navigateurs qui le supportent encore (ou pas du tout), bien sûr, il y a un polyfill pour cela. Mais dans l'ensemble, il s'agit d'une excellente technologie qui nous permet de faire toutes sortes de choses liées à la détection d'éléments dans une fenêtre tout en aidant à obtenir une très bonne amélioration des performances.

Pourquoi IntersectionObserver est-il bon pour vous ?

  • IntersectionObserver est une API asynchrone non bloquante !
  • IntersectionObserver remplace nos auditeurs coûteux sur les événements de scroll ou de resize .
  • IntersectionObserver fait tous les calculs coûteux comme getClientBoundingRect() pour vous afin que vous n'ayez pas besoin de le faire.
  • IntersectionObserver suit le modèle structurel des autres observateurs, donc, théoriquement, devrait être facile à comprendre si vous connaissez le fonctionnement des autres observateurs.

Choses à garder à l'esprit

Si nous comparons les capacités d'IntersectionObserver au monde de window.addEventListener('scroll') d'où tout vient, il sera difficile de voir les inconvénients de cet Observateur. Alors, notons simplement certaines choses à garder à l'esprit à la place :

  • Oui, IntersectionObserver est une API asynchrone non bloquante. C'est bon à savoir ! Mais il est encore plus important de comprendre que le code que vous exécutez dans vos rappels ne sera pas exécuté de manière asynchrone par défaut, même si l'API elle-même est asynchrone. Il y a donc encore une chance d'éliminer tous les avantages que vous obtenez d' IntersectionObserver si les calculs de votre fonction de rappel rendent le thread principal insensible. Mais c'est une autre histoire.
  • Si vous utilisez IntersectionObserver pour le chargement paresseux des actifs (comme des images, par exemple), exécutez .unobserve(asset) après le chargement de l'actif.
  • IntersectionObserver peut détecter les intersections uniquement pour les éléments qui apparaissent dans la structure de mise en forme du document. Pour être clair : les éléments observables doivent générer une boîte et affecter d'une manière ou d'une autre la mise en page. Voici quelques exemples pour vous permettre de mieux comprendre :

    • Éléments avec display: none n'est exclu ;
    • opacity: 0 ou visibility:hidden crée la boîte (bien qu'invisible) pour qu'elle soit détectée ;
    • Éléments absolument positionnés avec width:0px; height:0px width:0px; height:0px sont bien. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negative top , left , etc.) and are cut out by parent's overflow: hidden won't be detected: their box is out of scope for the formatting structure.
IntersectionObserver: Now You See Me
IntersectionObserver: Now You See Me

I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:

  • Intersection Observer API on MDN;
  • IntersectionObserver polyfill;
  • IntersectionObserver polyfill as npm module;
  • Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
  • Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.

With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.