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

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

Intersection Observer v1 est l'une de ces API probablement universellement appréciées. Maintenant que Safari la prend également en charge, elle est enfin universellement utilisable dans tous les principaux navigateurs. Pour vous rafraîchir la mémoire sur l'API, je vous recommande de regarder Supercharged Microtip sur Intersection Observeer v1 de Surma, qui est intégré ci-dessous. Vous pouvez également lire l'article détaillé de Surma. Les utilisateurs ont utilisé Intersection Observer v1 pour de nombreux 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 de l'API Intersection Observer sur MDN. Pour rappel, voici à quoi ressemble l'API Intersection Observer v1 dans les cas les plus élémentaires:

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'));

Quels sont les problèmes liés à Intersection Observer v1 ?

Nous tenons à préciser que la version 1 de l'outil Intersection Observer est géniale, mais elle n'est pas parfaite. Dans certains cas particuliers, l'API ne répond pas aux attentes. Voyons cela de plus près. L'API Intersection Observer v1 peut vous indiquer quand un élément est défilé dans la fenêtre d'affichage, mais elle ne vous indique pas s'il est recouvert par un autre contenu de page (c'est-à-dire s'il est masqué) ni si son affichage visuel a été modifié par des effets visuels tels que transform, opacity, filter, etc., ce qui peut 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 étudiant de plus près. En revanche, il est impossible d'obtenir les mêmes informations si l'élément en question se trouve dans un iFrame tiers.

Pourquoi la visibilité est-elle si importante ?

Malheureusement, Internet est un endroit qui attire les acteurs malintentionnés aux pires intentions. Par exemple, un éditeur suspect qui diffuse des annonces au paiement par clic sur un site de contenu peut être incité à inciter les internautes à cliquer sur ses annonces afin d'augmenter le paiement publicitaire de l'éditeur (au moins pendant une courte période, jusqu'à ce que le réseau publicitaire les détecte). Généralement, ces annonces sont diffusées dans des cadres iFrame. Désormais, si l'éditeur souhaite inciter les utilisateurs à cliquer sur ce type d'annonces, il peut rendre les iFrames totalement transparents en appliquant une règle CSS iframe { opacity: 0; } et en superposant les iFrames à un élément attrayant, comme une vidéo de chat sur laquelle les utilisateurs voudraient réellement cliquer. C'est ce qu'on appelle le détournement de clic. Vous pouvez voir ce type d'attaque de détournement de clic en action dans la section supérieure de cette démonstration (essayez de "regarder" la vidéo de chat et activez le "mode truc"). Vous remarquerez que l'annonce affichée dans l'iFrame "pense" qu'elle a reçu des clics légitimes, même si elle était totalement transparente au moment où vous avez cliqué dessus.

Inciter un utilisateur à cliquer sur une annonce en lui appliquant un style transparent et en la superposant sur un élément attrayant

Comment l'outil 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 comme le ferait un être humain. En définissant une option dans le constructeur IntersectionObserver, l'intersection d'instances IntersectionObserverEntry contiendra un nouveau champ booléen nommé isVisible. Une valeur true pour isVisible est une bonne garantie de la part de l'implémentation sous-jacente que l'élément cible est complètement masqué par d'autres contenus et qu'aucun effet visuel n'est appliqué qui modifierait ou déformerait son affichage à l'écran. En revanche, une valeur false signifie que l'implémentation ne peut pas garantir cette garantie.

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

Cela dit, les faux positifs ne sont en aucun cas autorisés (c'est-à-dire, définir isVisible sur true lorsque l'élément cible n'est pas complètement visible ni modifié).

À quoi ressemble le nouveau code dans la pratique ?

Le constructeur IntersectionObserver utilise désormais deux propriétés de configuration supplémentaires: delay et trackVisibility. Le champ 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 suit les modifications de la visibilité d'une cible.

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

Selon les spec 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 efficace autre qu'une translation 2D ou une augmentation proportionnellement 2D, la cible est considérée comme invisible.

  • Si la cible ou tout élément de sa chaîne de blocs associée a une opacité effective autre que 1,0, la cible est considérée comme invisible.

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

  • Si l'implémentation ne peut pas garantir que la cible est complètement masquée par le reste du contenu de la page, elle est considérée comme invisible.

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

Vous trouverez ci-dessous un court exemple de code illustrant les nouvelles fonctionnalités de l'API. Vous pouvez voir sa logique de suivi des clics en action dans la deuxième section de la démonstration (mais maintenant, essayez de "regarder" la vidéo du chiot). N'oubliez pas d'activer à nouveau le "mode astuce" pour devenir immédiatement un éditeur douteux, et découvrez comment Intersection Observer v2 empêche le suivi des clics non légitimes sur des annonces. Cette fois-ci, la version 2 de l'outil Intersection Observer est là pour nous. 🎉

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 lu cet article, ainsi qu'à Stefan Zager pour avoir examiné et mis en œuvre la fonctionnalité dans Chrome. Image principale par Sergey Semin sur Unsplash.