Maintenant, vous me voyez : comment différer, charger paresseux et agir avec IntersectionObserver
Publié: 2022-03-10Il é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.
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.
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.
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.
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.
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.
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
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, laroot
est la fenêtre de votre navigateur mais peut vraiment être n'importe quel élément de votre DOM (vous définissez alors laroot
sur quelque chose commedocument.getElementById('your-element')
). Gardez cependant à l'esprit que les éléments que vous souhaitez observer doivent "vivre" dans l'arborescence DOM deroot
dans ce cas.
-
rootMargin
: définit la marge autour de votre élémentroot
qui étend ou rétrécit le "cadre de capture" lorsque les dimensions de votreroot
ne fournissent pas suffisamment de flexibilité. Les options pour les valeurs de cette configuration sont similaires à celles demargin
en CSS, telles querootMargin: '50px 20px 10px 40px'
(top, right bottom, left). Les valeurs peuvent être abrégées (commerootMargin: '50px'
) et peuvent être exprimées enpx
ou%
. Par défaut,rootMargin: '0px'
.
-
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 deroot
etrootMargin
). Lethreshold
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 duthreshold
(je sais que cela peut parfois prêter à confusion), voici quelques exemples :-
threshold: 0
: La valeur par défautIntersectionObserver
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'espritIntersectionObserver
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
.
-
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 deroot
. -
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.
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é.
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'esttrue
, alors l'élément entre dans le « cadre de capture » ; - Si c'est le contraire et que c'est
false
maintenant alors que c'étaittrue
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.
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.
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.
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é.
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.
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) { … } });
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); … }
preloadImage()
est une fonction très simple qui ne mérite pas d'être mentionnée ici. Il suffit de lire la source.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 avecelement.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 deinline-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.
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".
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 descroll
ou deresize
. -
IntersectionObserver
fait tous les calculs coûteux commegetClientBoundingRect()
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
ouvisibility: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 negativetop
,left
, etc.) and are cut out by parent'soverflow: hidden
won't be detected: their box is out of scope for the formatting structure.
- Éléments avec
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.