Qu’ont en commun le défilement infini, le lazy loading (ou chargement à la demande) et la publicité en ligne ?

Ils ont tous besoin de connaître - et de réagir à - l’état de visibilité des éléments d’une page.

Malheureusement, savoir si un élément est visible a toujours été une chose compliquée sur le Web. La plupart des solutions écoutent les évènements scroll et resize (défilement et redimensionnement), puis utilisent des API du DOM telles que getBoundingClientRect() afin de calculer manuellement où les éléments se situent par rapport à la fenêtre visible. En général, cette méthode fonctionne mais elle n’est pas très efficace et ne prend pas en compte les autres facteurs qui peuvent modifier la visibilité, par exemple une image qui finit de se charger plus haut dans la page, décalant tout le reste vers le bas.

Les choses sont particulièrement compliquées avec la publicité, car il y a de l’argent en jeu. Comme l’explique Malte Ubl dans sa présentation à JSConf Iceland, les annonceurs ne veulent pas payer pour des publicités qui ne sont jamais vues. Pour s’assurer qu’elles soient vues, ils les recouvrent de dizaines d’animation Flash de 1 pixel, dont la visibilité peut être déduite à partir du framerate. Pour les systèmes qui ne disposent pas de Flash, comme les téléphones, des timers sont ajoutés pour forcer les navigateurs à recalculer la position de chaque publicité toutes les quelques millisecondes.

Ces techniques nuisent beaucoup à la performance, vident les batteries et seraient complètement inutiles si le navigateur pouvait simplement nous informer lorsque la visibilité d’un élément change.

C’est ce que fait l’API IntersectionObserver.

Et voici new IntersectionObserver()

Dans le cas le plus simple, utiliser l’API IntersectionObserver ressemble à :

let observer = new IntersectionObserver(handler);
observer.observe(target); // <— Elément à écouter

La démonstration ci-dessous illustre le fonctionnement un gestionnaire d’évènement simple. Un seul observateur peut surveiller de nombreux éléments simultanément ; il suffit de répéter la commande observer.observe() pour chaque cible.

See the Pen Voici IntersectionObserver by SphinxKnight (@SphinxKnight) on CodePen.

« Intersection » ? et la visibilité ?

Par défaut, un objet IntersectionObserver calcule dans quelle proportion une cible chevauche la portion visible de la page.

Illustration d'un élément cible avec une intersection partielle sur le viewport du navigateur

Mais un objet IntersectionObserver peut aussi surveiller dans quelle mesure une cible chevauche un élément parent défini de façon arbitraire, quelle que ce soit la réelle visibilité de cet élément. Ceci peut être utile pour des widgets qui chargent le contenu à la demande comme une liste défilant à l’infini à l’intérieur d’un <div> et qui pourrait utiliser un objet IntersectionObserver pour charger juste assez de contenu afin de remplir son conteneur.

Par souci de simplicité, la suite de cet article parlera de visibilité, mais gardez en tête qu’il ne s’agit pas forcément de visibilité au sens littéral du terme.

Gestionnaires simples

Les gestionnaires associés à un observateur sont des callbacks qui acceptent deux arguments :

  • Une liste d’objets IntersectionObserverEntry contenant chacun les métadonnées indiquant les changements de la zone de chevauchement d’une cible depuis le dernier appel du gestionnaire
  • Une référence à l’observateur.

Par défaut, les observateurs surveillent la fenêtre du navigateur. Cela signifie que la démo ci-dessus doit uniquement consulter la propriété isIntersecting pour déterminer si une partie d’une cible est visible.

Par défaut, les gestionnaires sont appelés uniquement lorsque les éléments cibles passent d’un état non visible à un état partiellement visible ou vice versa mais comment faire si l’on souhaite distinguer les éléments partiellement visibles des éléments visibles dans leur totalité ?

Les seuils sont là pour ça !

Travailler avec les seuils

En plus du callback, le constructeur d’un IntersectionObserver peut recevoir un objet avec différentes options de configuration. L’une de ces options est threshold, qui définit les valeurs limites pour l’invocation du gestionnaire.

let observer = new IntersectionObserver(handler, {
    threshold: 0 // <-- This is the default 
});

La valeur par défaut de threshold est 0; ce qui revient à invoquer le gestionnaire lorsqu’une cible devient partiellement visible ou complètement invisible. Régler cette valeur à 1 provoque le déclenchement du gestionnaire lorsque l’élément cible passe d’un état complètement visible à un état partiellement visible, tandis qu’une valeur de 0.5 déclenche le gestionnaire lorsque la cible passe le seuil de 50% de visibilité, dans l’une ou l’autre direction.

Il est possible de spécifier un tableau de seuils, comme on le voit avec le paramètre threshold: [0, 1] dans l’exemple ci-après :

See the Pen IntersectionObserver Thresholds by SphinxKnight (@SphinxKnight) on CodePen.

Faites défiler doucement la cible et observez son comportement.

Dans un premier temps la cible est entièrement visible —intersectionRatio vaut alors 1. La valeur change deux fois pendant qu’elle défile vers l’extérieur de l’écran : d’abord avec une valeur comme 0.87 puis pour avec une valeur égale à 0. Lorsque la cible revient dans la zone visible, intersectionRatio devient 0.05, puis 1. Les valeurs 0 et 1 sont plutôt simples à comprendre, mais d’où viennent les deux autres et qu’en est-il de toutes les valeurs comprises entre 0 et 1 ?

Les seuils sont définis en terme de transitions : le gestionnaire est appelé dès que le navigateur se rend compte que la valeur intersectionRatio d’une cible a passé le seuil. Définir les seuils à [0, 1] indique au navigateur « préviens-moi quand une cible passe les lignes d’invisibilité (0) et de visibilité complète (1) ». Cela définit donc trois états : complètement visible, partiellement visible et invisible.

La valeur qu’on observe pour intersectionRatio varie d’un test à l’autre car le navigateur doit attendre une période d’inactivité afin de vérifier et notifier les intersections. Ce type de calcul est traité en tâche de fond, avec une priorité inférieure au défilement et à l’interaction utilisateur.

Essayez d’éditer le codepen pour ajouter ou enlever les seuils et observez les changements dans la façon dont le gestionnaire est appelé.

Autres options

Le constructeur d’un IntersectionObserver accepte deux autres options :

  • root : la surface à observer (par défaut: la fenêtre du navigateur)
  • rootMargin : une chaîne de caractères qui indique quelle zone considérer en plus ou en moins par rapport à la taille logique de l’élément root lorsqu’on calcule les intersections (par défaut, c’est "0px 0px 0px 0px" qui sera utilisée).

Modifier root permet à un observateur de surveiller l’intersection avec un élément parent donné plutôt qu’avec la fenêtre du navigateur.

Utiliser la propriété rootMargin permet de détecter lorsqu’une cible s’approche de la zone surveillée. Par exemple, un observateur pourrait déclencher le chargement des images juste avant qu’elles ne deviennent visibles.

Prise en charge des navigateurs

L’API IntersectionObserver est disponible par défaut dans Edge 15, Chrome 51, et Firefox 55.

Un polyfill qui fonctionne avec tous les navigateurs est disponible, mais la performance obtenue est bien moins bonne qu’avec les implémentations natives.

Autres ressources