La confiance est bonne, l'observation est préférable: Intersection Observer v2

Intersection Observer v2 permet d'observer non seulement les intersections en soi, mais aussi de détecter si l'élément concerné était visible au moment de l'intersection.

Intersection Observer v1 est l'une de ces API universellement appréciées. Maintenant que Safari est également compatible avec elle, elle est enfin utilisable dans tous les principaux navigateurs. Pour vous rafraîchir la mémoire sur l'API, nous vous recommandons de regarder la vidéo Supercharged Microtip de Surma sur Intersection Observer v1, qui est intégrée ci-dessous. Vous pouvez également lire l'article détaillé de Surma. Intersection Observer v1 a été utilisé pour un large éventail de cas d'utilisation, comme le chargement différé d'images et de vidéos, l'envoi de notifications lorsque des éléments atteignent position: sticky, le déclenchement d'événements d'analyse, et bien plus encore.

Pour en savoir plus, consultez la documentation Intersection Observer sur MDN. Pour rappel, voici à quoi ressemble l'API Intersection Observer v1 dans le cas le plus simple :

const onIntersection = (entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log(entry);
    }
  }
};

const observer = new IntersectionObserver(onIntersection);
observer.observe(document.querySelector('#some-target'));

Quelles sont les difficultés liées à Intersection Observer v1 ?

Pour être clair, la version 1 d'Intersection Observer est excellente, mais elle n'est pas parfaite. Il existe des cas particuliers où l'API n'est pas à la hauteur. Voyons cela de plus près. L'API Intersection Observer v1 peut vous indiquer quand un élément est défilé dans le viewport de la fenêtre, mais elle ne vous indique pas si l'élément est recouvert par un autre contenu de la page (c'est-à-dire lorsqu'il est masqué) ni si son affichage visuel a été modifié par des effets visuels tels que transform, opacity, filter, etc., qui peuvent effectivement le rendre invisible.

Pour un élément du document de premier niveau, ces informations peuvent être déterminées en analysant le DOM via JavaScript, par exemple via DocumentOrShadowRoot.elementFromPoint(), puis en approfondissant l'analyse. En revanche, il est impossible d'obtenir les mêmes informations si l'élément en question se trouve dans une iframe tierce.

Pourquoi la visibilité réelle est-elle si importante ?

Internet est malheureusement un endroit qui attire les acteurs malintentionnés aux pires intentions. Par exemple, un éditeur douteux qui diffuse des annonces au paiement par clic sur un site de contenu peut être incité à inciter les internautes à cliquer sur leurs annonces afin d'augmenter les revenus publicitaires de l'éditeur (au moins pendant une courte période, jusqu'à ce que le réseau publicitaire les attrape). En règle générale, ces annonces sont diffusées dans des iFrames. Si l'éditeur souhaite inciter les utilisateurs à cliquer sur de telles annonces, il peut rendre les iFrames de l'annonce totalement transparents en appliquant une règle CSS iframe { opacity: 0; } et en superposant les cadres iFrame à un élément attrayant, comme une vidéo de chat mignon sur laquelle les utilisateurs voudraient cliquer. C'est ce qu'on appelle le détournement de clic. Vous pouvez voir une telle attaque de hameçonnage par clic en action dans la section supérieure de cette démo (essayez de "regarder" la vidéo sur les chats et activez le "mode piège"). Vous remarquerez que l'annonce contenue dans l'iFrame "pense" avoir reçu des clics légitimes, même si elle était totalement transparente lorsque vous avez cliqué dessus (presque involontairement).

Inciter un utilisateur à cliquer sur une annonce en la rendant transparente et en la superposant à un élément attrayant

Comment Intersection Observer v2 résout-il ce problème ?

Intersection Observer v2 introduit le concept de suivi de la "visibilité" réelle d'un élément cible tel qu'un être humain le définirait. En définissant une option dans le constructeur IntersectionObserver, les instances IntersectionObserverEntry qui se croisent contiennent alors un nouveau champ booléen nommé isVisible. Une valeur true pour isVisible est une bonne garantie par rapport à l'implémentation sous-jacente que l'élément cible n'est pas complètement masqué par d'autres contenus et ne comporte aucun effet visuel qui modifierait ou déformer son affichage à l'écran. En revanche, une valeur false signifie que l'implémentation ne peut pas garantir cela.

Un détail important de la spécification est que l'implémentation est autorisée à signaler des faux négatifs (c'est-à-dire à définir isVisible sur false même lorsque l'élément cible est complètement visible et non modifié). Pour des raisons de performances ou d'autres, les navigateurs se limitent à utiliser des rectangles de délimitation et une géométrie rectiligne. Ils ne tentent pas d'obtenir des résultats précis au pixel près pour les modifications telles que border-radius.

Cela dit, les faux positifs ne sont pas autorisés quelles que soient les circonstances (c'est-à-dire, définir isVisible sur true lorsque l'élément cible n'est pas entièrement visible ni modifié).

En pratique, à quoi ressemble le nouveau code ?

Le constructeur IntersectionObserver utilise désormais deux propriétés de configuration supplémentaires : delay et trackVisibility. delay est un nombre indiquant le délai minimal en millisecondes entre les notifications de l'observateur pour une cible donnée. trackVisibility est une valeur booléenne indiquant si l'observateur suivra les modifications de la visibilité d'une cible.

Il est important de noter que lorsque trackVisibility est true, delay doit être d'au moins 100 (c'est-à-dire une notification au maximum toutes les 100 ms). Comme indiqué précédemment, la visibilité est coûteuse à calculer. Il s'agit d'une précaution contre la dégradation des performances et l'utilisation de la batterie. Le développeur responsable utilisera la valeur tolérable la plus élevée pour le retard.

Selon les spécifications actuelles, la visibilité est calculée comme suit:

  • Si l'attribut trackVisibility de l'observateur est false, la cible est considérée comme visible. Cela correspond au comportement actuel de la version 1.

  • Si la cible possède une matrice de transformation effective autre qu'une translation 2D ou une mise à l'échelle 2D proportionnelle, elle est considérée comme invisible.

  • Si la cible ou un élément de la chaîne de blocs qui la contient présente une opacité effective autre que 1,0, la cible est considérée comme invisible.

  • Si des filtres sont appliqués à la cible ou à un élément de la chaîne de blocs qui la contient, la cible est considérée comme invisible.

  • Si l'implémentation ne peut pas garantir que la cible n'est pas complètement masquée par d'autres contenus de la page, la cible est considérée comme invisible.

Cela signifie que les implémentations actuelles sont assez conservatrices et qu'elles garantissent la visibilité. Par exemple, appliquer un filtre en niveaux de gris presque imperceptible comme filter: grayscale(0.01%) ou définir une transparence presque invisible avec opacity: 0.99 rendrait l'élément invisible.

Vous trouverez ci-dessous un exemple de code abrégé qui illustre les nouvelles fonctionnalités de l'API. Vous pouvez voir son fonctionnement dans la deuxième section de la démo (mais pour l'instant, essayez de "regarder" la vidéo sur les chiots). Veillez à réactiver le mode "trick" pour vous transformer immédiatement en éditeur douteux et voir comment Intersection Observer v2 empêche le suivi des clics sur les annonces non légitimes. Cette fois, Intersection Observer v2 est là pour nous aider. 🎉

Intersection Observer v2 empêche les clics accidentels sur une annonce.

<!DOCTYPE html>
<!-- This is the ad running in the iframe -->
<button id="callToActionButton">Buy now!</button>
// This is code running in the iframe.

// The iframe must be visible for at least 800ms prior to an input event
// for the input event to be considered valid.
const minimumVisibleDuration = 800;

// Keep track of when the button transitioned to a visible state.
let visibleSince = 0;

const button = document.querySelector('#callToActionButton');
button.addEventListener('click', (event) => {
  if ((visibleSince > 0) &&
      (performance.now() - visibleSince >= minimumVisibleDuration)) {
    trackAdClick();
  } else {
    rejectAdClick();
  }
});

const observer = new IntersectionObserver((changes) => {
  for (const change of changes) {
    // ⚠️ Feature detection
    if (typeof change.isVisible === 'undefined') {
      // The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
      change.isVisible = true;
    }
    if (change.isIntersecting && change.isVisible) {
      visibleSince = change.time;
    } else {
      visibleSince = 0;
    }
  }
}, {
  threshold: [1.0],
  // 🆕 Track the actual visibility of the element
  trackVisibility: true,
  // 🆕 Set a minimum delay between notifications
  delay: 100
}));

// Require that the entire iframe be visible.
observer.observe(document.querySelector('#ad'));

Remerciements

Merci à Simeon Vincent, Yoav Weiss et Mathias Bynens pour avoir relu cet article, ainsi qu'à Stefan Zager pour avoir examiné et implémenté la fonctionnalité dans Chrome. Image principale par Sergey Semin sur Unsplash.