Vertrauen ist gut, Beobachtung ist besser: Intersection Observer v2

Mit Intersection Observer v2 können Sie nicht nur Überschneidungen beobachten, sondern auch erkennen, ob das sich überschneidende Element zum Zeitpunkt der Überschneidung sichtbar war.

Intersection Observer v1 ist eine dieser APIs, die wahrscheinlich überall beliebt sind. Da sie jetzt auch von Safari unterstützt wird, ist sie endlich in allen wichtigen Browsern universell einsetzbar. Wenn Sie Ihr Wissen über die API auffrischen möchten, empfehle ich Ihnen, sich den unten eingebetteten Supercharged Microtip von Surma zu Intersection Observer v1 anzusehen. Hier finden Sie einen ausführlichen Artikel von Surma. 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, und das Auslösen von Analyseereignissen.

Die vollständigen Details finden Sie in der Intersection Observer-Dokumentation auf MDN. Hier ist eine kurze Erinnerung daran, wie die Intersection Observer v1 API im einfachsten Fall aussieht:

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 gut, aber nicht perfekt. Es gibt einige seltene Fälle, in denen die API nicht ausreicht. Sehen wir uns das genauer an. Mit der Intersection Observer v1 API lässt sich ermitteln, wann ein Element in den Viewport des Fensters gescrollt wird. Sie gibt jedoch nicht an, ob das Element durch andere Seiteninhalte verdeckt wird (d. h., ob das Element verdeckt ist) oder ob die visuelle Darstellung des Elements durch visuelle Effekte wie transform, opacity, filter usw. geändert wurde, wodurch es effektiv unsichtbar werden kann.

Bei einem Element im Dokument der obersten Ebene können diese Informationen durch Analysieren des DOM über JavaScript ermittelt werden, z. B. über DocumentOrShadowRoot.elementFromPoint(), und dann durch weitere Analysen. Wenn sich das betreffende Element in einem Drittanbieter-Iframe befindet, können dieselben Informationen nicht abgerufen werden.

Warum ist die tatsächliche Sichtbarkeit so wichtig?

Das Internet ist leider ein Ort, der böswillige Akteure mit schlechten Absichten anzieht. Ein unseriöser Publisher, der Pay-per-Click-Anzeigen auf einer Inhaltswebsite schaltet, könnte beispielsweise versuchen, Nutzer dazu zu bringen, auf seine Anzeigen zu klicken, um die Werbeeinnahmen des Publishers zu steigern – 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 mithilfe einer CSS-Regel iframe { opacity: 0; } vollständig transparent machen und sie über etwas Attraktives legen, z. B. ein süßes Katzenvideo, auf das Nutzer tatsächlich klicken möchten. Das nennt man Clickjacking. Ein Beispiel für einen solchen Clickjacking-Angriff finden Sie im oberen Bereich dieser Demo. Versuchen Sie, sich das Katzenvideo anzusehen und den „Trickmodus“ zu aktivieren. Sie werden feststellen, dass die Anzeige im iFrame „denkt“, dass sie zulässige Klicks erhalten hat, auch wenn sie völlig transparent war, als Sie (scheinbar unabsichtlich) darauf geklickt haben.

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

Wie wird dieses Problem in Intersection Observer v2 behoben?

Mit Intersection Observer v2 wird das Konzept der Erfassung der tatsächlichen „Sichtbarkeit“ eines Zielelements eingeführt, wie sie von einem Menschen definiert wird. Wenn Sie eine Option im IntersectionObserver-Konstruktor festlegen, enthalten sich überschneidende IntersectionObserverEntry-Instanzen ein neues boolesches Feld mit dem Namen isVisible. Ein true-Wert für isVisible ist eine starke Garantie der zugrunde liegenden Implementierung, dass das Zielelement nicht durch andere Inhalte verdeckt wird und keine visuellen Effekte angewendet werden, die die Darstellung auf dem Bildschirm verändern oder verzerren. Ein false-Wert bedeutet hingegen, dass die Implementierung diese Garantie nicht geben kann.

Ein wichtiges Detail der Spezifikation ist, dass die Implementierung falsch negative Ergebnisse (d. h. das Festlegen von isVisible auf false, auch wenn das Zielelement vollständig sichtbar und unverändert ist) melden darf. Aus Leistungsgründen oder anderen Gründen arbeiten Browser nur mit Bounding Boxes und geradliniger Geometrie. Sie versuchen nicht, pixelgenaue Ergebnisse für Änderungen wie border-radius 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 Konfigurationsattribute: delay und trackVisibility. delay ist eine Zahl, die die Mindestverzögerung in Millisekunden zwischen Benachrichtigungen des Beobachters für ein bestimmtes Ziel angibt. trackVisibility ist ein boolescher Wert, der angibt, ob der Observer Änderungen an der Sichtbarkeit eines Ziels verfolgt.

Wichtig: Wenn trackVisibility gleich true ist, muss delay mindestens 100 betragen (d. h. maximal eine Benachrichtigung alle 100 ms). Wie bereits erwähnt, ist die Berechnung der Sichtbarkeit aufwendig. Diese Anforderung dient als Vorsichtsmaßnahme gegen Leistungseinbußen und hohen Akkuverbrauch. Der verantwortliche Entwickler verwendet den größten zulässigen Wert für die Verzögerung.

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

  • Wenn das Attribut trackVisibility des Beobachters false ist, gilt das Ziel als sichtbar. Dies entspricht dem aktuellen v1-Verhalten.

  • Wenn das Ziel eine effektive Transformationsmatrix hat, die nicht einer 2D-Übersetzung oder einer proportionalen 2D-Hochskalierung entspricht, gilt das Ziel als unsichtbar.

  • Wenn das Ziel oder ein Element in der zugehörigen Blockkette eine effektive Deckkraft ungleich 1,0 hat, gilt das Ziel als unsichtbar.

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

  • Wenn bei der Implementierung nicht garantiert werden kann, dass das Ziel nicht durch andere Seiteninhalte verdeckt wird, gilt es als unsichtbar.

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

Unten finden Sie ein kurzes Codebeispiel, das die neuen API-Funktionen veranschaulicht. Die Logik für das Klick-Tracking können Sie im zweiten Abschnitt der Demo in Aktion sehen. Versuchen Sie es jetzt mit dem Welpenvideo. Aktivieren Sie den „Trick Mode“ wieder, um sich sofort in einen unseriösen Publisher zu verwandeln und zu sehen, wie mit Intersection Observer v2 verhindert wird, dass unzulässige Anzeigenklicks erfasst werden. Dieses Mal hilft uns Intersection Observer v2. 🎉

Intersection Observer v2 verhindert unbeabsichtigte Klicks auf Anzeigen.

<!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 das Überprüfen dieses Artikels sowie an Stefan Zager für das Überprüfen und Implementieren der Funktion in Chrome. Hero-Image von Sergey Semin auf Unsplash.