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 szybko odświeżyć sobie informacje o tym interfejsie API, obejrzyj Surma Możesz też przeczytać obszerny artykuł Surmy. 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 w wersji 1 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 API Intersection Observer v1 może poinformować, kiedy element jest przewinięty do widocznego obszaru okna, ale nie poinformuje, czy element jest zasłonięty przez jakikolwiek inny element strony (czyli czy jest zasłonięty) ani czy wyświetlanie elementu nie zostało zmodyfikowane przez efekty wizualne takie jak transform, opacity, filter itp., które skutecznie mogą sprawić, że element będzie niewidoczny.

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 takie reklamy są wyświetlane w ramkach 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 uroczy filmik z kotem, który użytkownicy chętnie klikną. 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 elementach iframe „myśli”, że uzyskała prawidłowe kliknięcia, nawet jeśli była całkowicie przezroczysta, gdy ją (pozornie nieumyślnie) kliknąłeś.

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

Jak Intersection Observer 2 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. Z kolei wartość false oznacza, że implementacja nie może zagwarantować tego zapewnienia.

Ważnym szczegółem specyfikacji jest to, że implementacja może zgłaszać fałszywie wyniki negatywne (czyli ustawiać isVisible na false nawet wtedy, gdy element docelowy jest całkowicie widoczny i niemodyfikowany). 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łszywie pozytywne wyniki nie są dozwolone pod żadnym pozorem (czyli ustawienie wartości isVisible na true, gdy element docelowy nie jest całkowicie widoczny i niezmodyfikowany).

Jak wygląda nowy kod 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.

Należy pamiętać, ż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 powinien użyć największej dopuszczalnej wartości 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 może zagwarantować, że miejsce docelowe nie jest całkowicie zasłonięte przez inne treści na stronie, jest ono uważane za niewidoczne.

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

Poniżej znajdziesz krótki przykładowy kod, który ilustruje nowe funkcje interfejsu API. W drugiej części prezentacji możesz zobaczyć, jak działa śledzenie kliknięć (ale teraz spróbuj „obejrzeć” film z szczeniakami). 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. 🎉

Intersection Observer w wersji 2 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.