Zaufanie jest dobre, obserwacja jest lepsza: obserwacja Intersection Observer (wersja 2)

Obserwator połączeń w wersji 2 umożliwia nie tylko obserwowanie skrzyżowań na każde z nich, ale także wykrywanie, czy w momencie przecięcia element przecinający był widoczny.

Intersection Observer (wersja 1) to jeden z interfejsów API powszechnie lubianych, a obecnie obsługuje go również Safari, dzięki czemu można go używać we wszystkich najpopularniejszych przeglądarkach. Aby przypomnieć sobie, jak działa interfejs API, warto obejrzeć umieszczony poniżej film Supercharged Microtip autorstwa Surmy dotyczący Intersection Observer w wersji 1. Możesz też przeczytać szczegółowy artykuł Surmy. Użytkownicy wykorzystywali Intersection Observer v1 do wielu różnych celów, takich jak leniwe ładowanie obrazów i filmów, otrzymywanie powiadomień o osiągnięciu position: sticky czy uruchamianie zdarzeń analitycznych.

Szczegółowe informacje znajdziesz w dokumentacji Intersection Observer w MDN. Przypominam jednak, że w najbardziej podstawowym przypadku interfejs Intersection Observer API wygląda tak:

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

Co stanowi wyzwanie w przypadku Intersection Observer v1?

Intersection Observer v1 to świetny wynik, ale nie jest doskonały. Zdarzają się sytuacje, w których interfejs API nie działa. Przyjrzyjmy się temu bliżej! Interfejs Intersection Observer API w wersji 1 informuje o tym, czy element został przewinięty w widocznym obszarze okna, ale nie informuje, czy element jest zasłonięty przez jakąkolwiek inną treść na stronie (czyli czy element jest zasłonięty) lub czy element wizualny został zmodyfikowany przez efekty wizualne, takie jak transform, opacity, filter itp., które skutecznie mogą go ukryć.

W przypadku elementu w dokumencie najwyższego poziomu informacje te można określić, analizując DOM za pomocą JavaScriptu, np. za pomocą narzędzia DocumentOrShadowRoot.elementFromPoint(), a potem pogłębiając analizę. Tych informacji nie można natomiast uzyskać, jeśli dany element znajduje się w zewnętrznym elemencie iframe.

Dlaczego widoczność jest tak ważna?

Internet jest niestety miejscem, które przyciąga nieuczciwe podmioty z gorszymi zamiarami. Na przykład niepewny wydawca, który wyświetla reklamy płatne za kliknięcie w witrynie w sieci reklamowej, może być zachęcany do nakłaniania użytkowników do klikania reklam w celu zwiększenia zarobków z reklamy (przynajmniej na krótki okres, dopóki sieć reklamowa ich nie zauważy). Zwykle tego typu reklamy są wyświetlane w elementach iframe. Jeśli wydawca chciał, by użytkownicy klikali takie reklamy, może sprawić, że elementy iframe reklam staną się całkowicie przezroczyste, stosując regułę CSS iframe { opacity: 0; } i nakładając elementy iframe na coś atrakcyjnego, np. filmu z uroczym kotkiem, który użytkownicy chcieliby kliknąć. Jest to tzw. przechwytywanie kliknięć. Sposób działania takiego ataku możesz zobaczyć w górnej części tej prezentacji (spróbuj „obejrzeć” film z kotami i włącz „tryb sztuczki”). Zauważysz, że reklama w elemencie iframe „uważa”, że uzyskała prawidłowe kliknięcia, nawet jeśli była całkowicie przejrzysta w momencie kliknięcia (umyślnie nieumyślnie).

Nakłanianie użytkownika do kliknięcia reklamy przez nadanie jej przezroczystości i nałożenie jej na coś atrakcyjnego.

Jak pomaga to rozwiązać problem z obserwacją Intersection Observer (wersja 2)?

Intersection Observer (2) wprowadza koncepcję śledzenia rzeczywistej „widoczności” elementu docelowego w takiej postaci, w jakiej może go zdefiniować człowiek. Po ustawieniu opcji w konstruktorze IntersectionObserver przecinające się instancje IntersectionObserverEntry będą zawierać nowe pole wartości logicznej o nazwie isVisible. Wartość true dla isVisible zapewnia wyraźną gwarancję w ramach podstawowej implementacji, że element docelowy jest całkowicie oddzielony od innych treści i nie ma żadnych efektów wizualnych, które mogłyby zmienić lub zniekształcić obraz na ekranie. Z kolei wartość false oznacza, że implementacja nie jest w stanie tego zagwarantować.

Ważną spec jest to, że implementacja zezwala na zgłaszanie fałszywych wyników negatywnych (czyli ustawienie isVisible na false nawet wtedy, gdy element docelowy jest w pełni widoczny i niezmodyfikowany). Ze względu na wydajność lub z innych powodów przeglądarki ograniczają się do pracy z ramkami ograniczającymi i prostoliniową geometrią. Ich stosowanie nie jest w stanie uzyskać perfekcyjnych wyników w przypadku modyfikacji takich jak border-radius.

Jednak niedozwolonefałszywie pozytywne wartości w żadnych okolicznościach (czyli ustawienie wartości isVisible na true, gdy element docelowy nie jest w pełni widoczny ani niezmodyfikowany).

Jak wygląda nowy kod w praktyce?

Konstruktor IntersectionObserver przyjmuje teraz 2 dodatkowe właściwości konfiguracji: delay i trackVisibility. delay to liczba wskazująca minimalne opóźnienie w milisekundach między powiadomieniami obserwatora w przypadku danego celu. trackVisibility to wartość logiczna wskazująca, czy obserwator będzie śledzić zmiany widoczności celu.

Pamiętaj, że gdy trackVisibility ma wartość true, delay musi mieć wartość co najmniej 100 (czyli nie więcej niż 1 powiadomienie co 100 ms). Jak już wspomnieliśmy, obliczanie widoczności jest kosztowne, a to wymaganie stanowi zabezpieczenie przed spadkiem wydajności i zużyciem baterii. Odpowiedzialny programista użyje największej dopuszczalnej wartości opóźnienia.

Zgodnie z obecną spec widoczność jest obliczana w ten sposób:

  • Jeśli atrybut trackVisibility obserwatora to false, wartość docelowa jest uznawana za widoczną. Odpowiada to obecnemu działaniu w wersji 1.

  • Jeśli cel ma efektywną macierz przekształceń inną niż przesunięcie 2D lub proporcjonalne skalowanie 2D, cel jest uważany za niewidoczny.

  • Jeśli cel lub dowolny element w jego łańcuchu bloków ma efektywną przezroczystość inną niż 1,0, jest on uznawany za niewidoczny.

  • Jeśli w miejscu docelowym lub w jego zawierającym łańcuchu blokad zastosowano jakiekolwiek filtry, jest on uznawany za niewidoczny.

  • Jeśli implementacja nie może zagwarantować, że cel jest całkowicie niewidoczny przez inne treści strony, jest on uważany za niewidoczny.

Oznacza to, że obecne implementacje są dość zachowawcze i gwarantują widoczność. Na przykład zastosowanie prawie niewidocznego filtra skali szarości, takiego jak filter: grayscale(0.01%), lub ustawienie niemal niewidocznej przezroczystości za pomocą opacity: 0.99 spowoduje, że element stanie się niewidoczny.

Poniżej znajduje się krótki przykład kodu ilustrujący nowe funkcje interfejsu API. Ich działanie możesz zobaczyć w drugiej sekcji prezentacji (ale teraz możesz po prostu „obejrzeć” film ze szczeniaczkami). Aktywuj ponownie „tryb nieuczciwego”, aby od razu zamienić się w podejrzanego wydawcę i sprawdzić, jak Intersection Observer (wersja 2) zapobiega śledzeniu nierzetelnych kliknięć reklam. Tym razem do akcji wkracza Intersection Observer v2. 🎉

Obserwator połączeń (wersja 2) zapobiegający niezamierzonemu kliknięciu reklamy.

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

Podziękowania

Dziękujemy Simeonowi Vincentowi, Yoavowi Weissowi i Mathiasowi Bynensowi za przeczytanie tego artykułu, a także Stefanowi Zagerowi za sprawdzenie i zaimplementowanie tej funkcji w Chrome. Baner powitalny autorstwa Sergeya Semina w serwisie Unsplash.