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

Intersection Observer w wersji 2 umożliwia nie tylko obserwowanie przecięć, ale też wykrywanie, czy element przecinający był widoczny w momencie przecięcia.

Interfejs Intersection Observer w wersji 1 jest jednym z tych interfejsów API, które są powszechnie lubiane. Teraz, gdy Safari go obsługuje, można go w końcu stosować we wszystkich głównych przeglądarkach. Aby przypomnieć sobie, jak działa interfejs API, zachęcam do obejrzenia materiału na temat Supercharged Microtip kanału Surma na kanale Intersection Observer v1. Znajdziesz go poniżej. Możesz też przeczytać szczegółowy artykuł w serwisie Surma. Interfejs Intersection Observer w wersji 1 jest używany w wielu przypadkach, np. do opóźnionego wczytywania obrazów i filmów, powiadamiania o dotarciu elementów do pozycji position: sticky, wywoływania zdarzeń analitycznych i wielu innych.

Pełne informacje znajdziesz w dokumentacji Intersection Observer na stronie MDN. Poniżej przedstawiamy interfejs Intersection Observer API w najprostszej postaci:

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

Jakie są trudności związane z wersją 1 metody IntersectionObserver?

Po pierwsze, IntersectionObserver w wersji 1 jest świetny, ale nie jest idealny. W niektórych szczególnych przypadkach interfejs API może nie działać prawidłowo. Przyjrzyjmy się temu bliżej. Interfejs Intersection Observer v1 API poinformuje Cię, gdy element znajdzie się w widocznym obszarze okna, ale nie dowiesz się, czy jest on przysłonięty przez inną zawartość strony (tzn. czy jest zasłonięty) oraz czy jego wygląd został zmodyfikowany przez efekty wizualne takie jak transform, opacity czy filter, co skutecznie uczyni go niewidocznym.

W przypadku elementu w dokumencie najwyższego poziomu można określić te informacje, analizując DOM za pomocą JavaScriptu, np. za pomocą DocumentOrShadowRoot.elementFromPoint(), a następnie zagłębiając się w strukturę dokumentu. Nie można natomiast uzyskać tych samych informacji, jeśli dany element znajduje się w ramce iframe firmy zewnętrznej.

Dlaczego widoczność jest tak ważna?

Internet jest niestety miejscem, które przyciąga nieuczciwych podmiotów o złych zamiarach. Na przykład podejrzany wydawca, który wyświetla reklamy typu „pay-per-click” w witrynie z treściami, może być zachęcany do zmuszania użytkowników do klikania reklam w celu zwiększenia zarobków z reklam (przynajmniej przez krótki czas, dopóki sieć reklamowa go nie wykryje). Zwykle tego typu reklamy są wyświetlane w elementach iframe. Jeśli wydawca chce zachęcić użytkowników do klikania takich reklam, może sprawić, że ramki iframe reklam będą całkowicie przezroczyste, stosując regułę CSS iframe { opacity: 0; } i nakładając je na coś atrakcyjnego, np. na uroczego kota, który zachęca użytkowników do kliknięcia. Jest to tzw. clickjacking. Taki atak typu clickjacking możesz zobaczyć w górnej części tego demonstracji (spróbuj „obejrzeć” film z kotem i aktywować „tryb oszutek”). Zauważysz, że reklama w elemencie iframe „uważa”, że uzyskała prawidłowe kliknięcia, mimo że kliknięcie przez Ciebie (pozornie nieświadomie) było całkowicie przejrzyste.

Wmanewrowanie użytkownika do kliknięcia reklamy przez nadanie jej przezroczystego wyglądu i nałożenie na coś atrakcyjnego.

Jak IntersectionObserver v2 rozwiązuje ten problem?

W Intersection Observer v2 wprowadzono pojęcie śledzenia rzeczywistej „widoczności” elementu docelowego w sposób zrozumiały dla człowieka. Po ustawieniu opcji w konstruktorze IntersectionObserver instancje przecinających się IntersectionObserverEntry będą zawierać nowe pole logiczne o nazwie isVisible. Wartość true dla parametru isVisible jest mocną gwarancją implementacji podstawowej, że element docelowy jest całkowicie widoczny i nie jest zasłonięty przez inne treści. Nie ma też zastosowanych efektów wizualnych, które mogłyby zmienić lub zniekształcić jego wyświetlanie na ekranie. Wartość false oznacza natomiast, że implementacja nie gwarantuje tej gwarancji.

Ważną cechą specyfikacji jest to, że implementacja może zgłaszać wyniki fałszywie negatywne (czyli ustawić isVisible na false, nawet jeśli 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 obcięciami i geometrią prostokątną; nie próbują uzyskać idealnych wyników w przypadku modyfikacji takich jak border-radius.

Fałszywe trafienia są jednak niedozwolone w żadnym przypadku (czyli nie można ustawić isVisible na true, gdy element docelowy nie jest w pełni widoczny ani niezmodyfikowany).

Jak nowy kod wygląda w praktyce?

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

Pamiętaj, że gdy trackVisibility to true, delay musi wynosić co najmniej 100 (czyli nie więcej niż 1 powiadomienie na każde 100 ms). Jak już wspomnieliśmy, obliczenie widoczności jest kosztowne, a to wymaganie ma na celu zapobieganie spadkowi wydajności i zwiększonemu zużyciu baterii. Odpowiedzialny deweloper zastosuje największą dopuszczalną wartość opóźnienia.

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

  • Jeśli atrybut trackVisibility obserwatora ma wartość false, cel jest uważany za widoczny. Odpowiada to obecnemu działaniu wersji 1.

  • Jeśli docel ma efektywną macierz przekształcenia inną niż przesunięcie 2D lub proporcjonalne powiększenie 2D, jest ono uważane za niewidoczne.

  • Jeśli element docelowy lub dowolny element w łańcuchu bloków go zawierającym ma skuteczną przezroczystość inną niż 1,0, jest on uważany za niewidoczny.

  • Jeśli do celu lub dowolnego elementu w łańcuchu bloków zawierającym ten cel zastosowano jakiekolwiek filtry, cel jest uważany za niewidoczny.

  • Jeśli implementacja nie gwarantuje, że miejsce docelowe jest całkowicie wykluczone z innej zawartości strony, jest on uważany za niewidoczny.

Oznacza to, że obecne implementacje są dość konserwatywne, jeśli chodzi o gwarantowanie widoczności. Na przykład zastosowanie niemal niezauważalnego filtra skali szarości, takiego jak filter: grayscale(0.01%), lub ustawienie niemal niewidocznej przezroczystości za pomocą ustawienia opacity: 0.99 sprawi, że element będzie niewidoczny.

Poniżej znajduje się krótki przykładowy kod ilustrujący nowe funkcje interfejsu API. W drugiej części prezentacji możesz zobaczyć, jak działa śledzenie kliknięć (ale teraz spróbuj „obejrzeć” film z szczeniakiem). Pamiętaj, aby ponownie aktywować „tryb sztuczny”, aby od razu przekształcić się w podejrzanego wydawcę i sprawdzić, jak Intersection Observer v2 zapobiega śledzeniu nielegalnych kliknięć reklam. Tym razem Intersection Observer v2 nam pomoże. 🎉

Obserwatorzy skrzyżowań w wersji 2, który zapobiega 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 WeissowiMathiasowi Bynensowi za sprawdzenie tego artykułu, a także Stefanowi Zagerowi za sprawdzenie i wdrażanie tej funkcji w Chrome. Obraz główny: Sergey Semin, Unsplash.