Vertrauen ist gut, Beobachtung ist besser: Intersection Observer v2

Intersection Observer v2 bietet die Möglichkeit, nicht nur Überschneidungen zu beobachten, sondern auch zu erkennen, ob das sich überschneidende Element zum Zeitpunkt der Überschneidung sichtbar war.

Intersection Observer v1 ist eine der APIs, die wahrscheinlich von allen geliebt wird. Jetzt, da sie auch von Safari unterstützt wird, kann sie endlich in allen gängigen Browsern verwendet werden. Wenn Sie sich noch einmal die API in Erinnerung rufen möchten, empfehle ich Ihnen, sich den Supercharged Microtip von Surma zu IntersectionObserver v1 anzusehen, der unten eingebettet ist. Weitere Informationen finden Sie auch in Surmas detaillierten Artikel. Intersection Observer v1 wurde für eine Vielzahl von Anwendungsfällen verwendet, z. B. für das Lazy-Loading von Bildern und Videos, Benachrichtigungen, wenn Elemente position: sticky erreichen oder Analysen-Ereignisse.

Ausführliche Informationen finden Sie in der Intersection Observer-Dokumentation auf MDN. Zur Erinnerung: So sieht die Intersection Observer v1 API im einfachsten Fall aus:

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

Was ist das Problem mit Intersection Observer v1?

Intersection Observer v1 ist zwar großartig, aber nicht perfekt. Es gibt jedoch einige seltene Fälle, in denen die API nicht ausreicht. Sehen wir uns das genauer an. Die Intersection Observer v1 API kann Ihnen mitteilen, wann ein Element in den Darstellungsbereich des Fensters gescrollt wird. Sie gibt jedoch nicht an, ob das Element von anderen Seiteninhalten verdeckt ist (d. h., ob es verdeckt ist) oder ob die visuelle Darstellung des Elements durch visuelle Effekte wie transform, opacity oder filter usw. geändert wurde, wodurch es praktisch unsichtbar werden kann.

Bei einem Element im Top-Level-Dokument können diese Informationen durch Analyse des DOM über JavaScript ermittelt werden, z. B. über DocumentOrShadowRoot.elementFromPoint(). Wenn sich das betreffende Element dagegen in einem Iframe eines Drittanbieters befindet, können diese Informationen nicht abgerufen werden.

Warum ist die tatsächliche Sichtbarkeit so wichtig?

Leider zieht das Internet auch böswillige Akteure mit schlechten Absichten an. So kann ein zwielichtiger Publisher, der auf einer Website mit Inhalten Pay-per-Click-Anzeigen schaltet, dazu verleitet werden, Nutzer dazu zu bringen, auf seine Anzeigen zu klicken, um die Werbeauszahlung des Publishers zu erhöhen (zumindest für kurze Zeit, bis das Werbenetzwerk ihn erwischt). Normalerweise werden solche Anzeigen in Iframes ausgeliefert. Wenn der Publisher Nutzer dazu bringen möchte, auf solche Anzeigen zu klicken, kann er die Anzeigen-Iframes durch Anwenden einer CSS-Regel iframe { opacity: 0; } vollständig transparent machen und die Iframes über etwas Attraktives legen, z. B. ein süßes Katzenvideo, auf das Nutzer tatsächlich klicken möchten. Dies wird als Clickjacking bezeichnet. Einen solchen Clickjacking-Angriff in Aktion sehen Sie im oberen Bereich dieser Demo. Sehen Sie sich das Katzenvideo an und aktivieren Sie den „Trick-Modus“. Sie werden feststellen, dass die Anzeige im iFrame „denkt“, dass sie legitime Klicks erhalten hat, auch wenn sie vollständig transparent war, als Sie (vorgeblich unfreiwillig) darauf geklickt haben.

Nutzer dazu bringen, auf eine Anzeige zu klicken, indem sie transparent gestaltet und über etwas Attraktives gelegt wird.

Wie wird dieses Problem mit Intersection Observer v2 behoben?

Mit Intersection Observer v2 wird das Konzept eingeführt, die tatsächliche „Sichtbarkeit“ eines Zielelements zu erfassen, wie ein Mensch sie definieren würde. Wenn Sie im IntersectionObserver-Konstruktor eine Option festlegen, enthalten sich überschneidende IntersectionObserverEntry-Instanzen ein neues boolesches Feld namens isVisible. Ein Wert „true“ für „isVisible“ ist eine starke Garantie der zugrunde liegenden Implementierung, dass das Zielelement nicht vollständig von anderen Inhalten verdeckt ist und keine visuellen Effekte angewendet werden, die die Darstellung auf dem Bildschirm verändern oder verzerren würden. Ein Wert von false bedeutet dagegen, dass die Implementierung diese Garantie nicht geben kann.

Ein wichtiges Detail der Spezifikation ist, dass die Implementierung falsch negative Ergebnisse melden darf, d. h. isVisible auf false setzen, auch wenn das Zielelement vollständig sichtbar und unverändert ist. Aus Leistungs- oder anderen Gründen beschränken sich Browser auf die Arbeit mit Begrenzungsboxen und rechtwinkliger Geometrie. Sie versuchen nicht, bei Modifikationen wie border-radius pixelgenaue Ergebnisse zu erzielen.

Falsch-positive Ergebnisse sind jedoch unter keinen Umständen zulässig. Das bedeutet, dass isVisible nicht auf true gesetzt werden darf, wenn das Zielelement nicht vollständig sichtbar und unverändert ist.

Wie sieht der neue Code in der Praxis aus?

Der Konstruktor IntersectionObserver verwendet jetzt zwei zusätzliche Konfigurationseigenschaften: delay und trackVisibility. delay ist eine Zahl, die die minimale Verzögerung in Millisekunden zwischen Benachrichtigungen vom Beobachter für ein bestimmtes Ziel angibt. trackVisibility ist ein boolescher Wert, der angibt, ob der Beobachter Änderungen an der Sichtbarkeit eines Ziels erfasst.

Wenn trackVisibility = true ist, muss delay mindestens 100 betragen, d. h. es darf nicht mehr als eine Benachrichtigung alle 100 Millisekunden geben. Wie bereits erwähnt, ist die Berechnung der Sichtbarkeit sehr aufwendig. Diese Anforderung dient als Schutz vor Leistungseinbußen und Akkuverbrauch. Der zuständige Entwickler verwendet den höchsten tolerierbaren Wert für die Verzögerung.

Gemäß der aktuellen Spezifikation wird die Sichtbarkeit so berechnet:

  • Wenn das trackVisibility-Attribut des Betrachters false ist, wird das Ziel als sichtbar betrachtet. Das entspricht dem aktuellen Verhalten von Version 1.

  • Wenn das Ziel eine andere effektive Transformationsmatrix als eine 2D-Verschiebung oder eine proportionale 2D-Aufwärtsskalierung hat, wird es als unsichtbar betrachtet.

  • Wenn das Ziel oder ein Element in der enthaltenen Blockkette eine andere effektive Deckkraft als 1,0 hat, gilt das Ziel als unsichtbar.

  • Wenn auf das Ziel oder ein Element in der enthaltenen Blockkette Filter angewendet werden, gilt das Ziel als unsichtbar.

  • Wenn bei der Implementierung nicht garantiert werden kann, dass das Ziel nicht vollständig von anderen Seiteninhalten verdeckt ist, gilt es als unsichtbar.

Das bedeutet, dass die aktuellen Implementierungen die Sichtbarkeit ziemlich konservativ garantieren. Wenn Sie beispielsweise einen fast unmerklichen Graustufenfilter wie filter: grayscale(0.01%) anwenden oder eine fast unsichtbare Transparenz mit opacity: 0.99 festlegen, wird das Element unsichtbar.

Unten findest du ein kurzes Codebeispiel, das die neuen API-Funktionen veranschaulicht. Die Klick-Tracking-Logik sehen Sie im zweiten Abschnitt der Demo. Sehen Sie sich aber zuerst das Video mit dem Welpen an. Aktivieren Sie den „Trickmodus“ noch einmal, um sich sofort in einen zwielichtigen Publisher zu verwandeln und zu sehen, wie Intersection Observer v2 verhindert, dass nicht legitime Anzeigenklicks erfasst werden. Dieses Mal sind wir mit Intersection Observer v2 gerüstet. 🎉

Intersection Observer v2 verhindert einen unbeabsichtigten Klick auf eine Anzeige.

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

Danksagungen

Vielen Dank an Simeon Vincent, Yoav Weiss und Mathias Bynens für die Überprüfung dieses Artikels sowie an Stefan Zager für die Überprüfung und Implementierung der Funktion in Chrome. Hero-Image von Sergey Semin auf Unsplash.