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 mit der API vertraut machen möchten, empfehle ich Ihnen den Supercharged Microtip von Surma zur IntersectionObserver v1, 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 Lazy Loading von Bildern und Videos, für Benachrichtigungen, wenn Elemente position: sticky erreichen, zum Auslösen von Analyseereignissen und vielem mehr.

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 einige Sonderfä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. Beispielsweise könnte ein zwielichtiger Publisher, der Pay-per-Click-Anzeigen auf einer Content-Website schaltet, Nutzer dazu verleiten, auf seine Anzeigen zu klicken, um die Anzeigenauszahlung des Publishers zu erhöhen (zumindest für einen kurzen Zeitraum, bis sie vom Werbenetzwerk erfasst werden). Solche Anzeigen werden normalerweise 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 kann Intersection Observer v2 dieses Problem beheben?

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 eine Option im IntersectionObserver-Konstruktor festlegen, enthalten die sich überschneidenden IntersectionObserverEntry-Instanzen ein neues boolesches Feld mit dem Namen 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 seine 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 teuer und diese Anforderung ist eine Vorsichtsmaßnahme gegen 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 Beobachters false ist, gilt das Ziel als sichtbar. Das entspricht dem aktuellen Verhalten von Version 1.

  • Wenn das Ziel eine andere effektive Transformationsmatrix als eine 2D-Verschiebung oder eine proportionale 2D-Hochskalierung hat, gilt das Ziel als unsichtbar.

  • Wenn das Ziel oder ein Element in seiner enthaltenden 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 bei der Gewährleistung der Sichtbarkeit ziemlich konservativ sind. 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. Im zweiten Abschnitt der Demo können Sie die Klick-Tracking-Logik in Aktion sehen. Versuchen Sie jetzt, das Welpenvideo anzusehen. 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 unterstützt Intersection Observer v2! 🎉

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