Intersection Observer w JavaScript

Intersection Observer to natywne API JavaScript, dzięki któremu można osiągnąć wiele przydatnych rozwiązań. To na nim oparte jest wiele bibliotek służących do optymalizacji strony.

Czy zdarzyło ci się kiedyś napotkać problem, którego rozwiązanie wymagałoby użycie jakichś zdarzeń scrollowania (onscroll). Przykład takiego code case to lazy loading lub jakieś animacje w momencie pojawienia się elementu we viewporcie (ekranie) użytkownika. Prawdopodobnie zdarzenie onscroll w takich przypadkach jest słabym rozwiązaniem jeżeli chodzi o performance aplikacji. Zauważ, że zdarzenie to wykonuje się za każdym razem, jak scroll przemierza swoją drogę po stronie. Jasne jest więc, że nie jest to zbyt dobre rozwiązanie dla procesora urządzenia, który musi nadążać nad reobliczaniem pozycji scrolla. W JS istnieje alternatywa do scroll listenera, czyli wspomniany Intersection Observer, którego mechanikę omówię w dzisiejszym artykule.

Jak działa Intersection Observer?

Jest to niezbyt rozbudowane API JavaScriptu. Jako, że niezbyt lubię pisać teoretycznie, omówimy sobie jego mechanikę na przukładzie. Zaczniemy od zdefiniowania HTML:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
    <title>Intersection Observer Training</title>
    <link rel="stylesheet" href="style.scss" />
  </head>
  <body>
    <img
      src="https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2018/11/capa.rurik_-300x300.png"
      alt=""
      class="image"
    />
    <script src="index.ts"></script>
  </body>
</html>

Mamy tu podpięty plik scss oraz skrypt z rozszerzeniem ts. Na takie rzeczy pozwala bundler Parcel, który jest świetny do niedużych aplikacji. Ponadto mamy tutaj obrazek o klasie image i wymiarach 300x300. Tak wyglądają style: 

// styles.scss
html {
    margin: 0;
}

body {
    height: 300vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

.image {
    transform: translateX(-500px);
    opacity: 0;
    transition: transform 0.6s ease-in-out, opacity 0.6s ease-in-out;

    &.transitioned {
        transform: translateX(0);
        opacity: 1;
    }
}

body posiada równowartość wysokości trzech ekranów użytkownika, tak by bez przeszkód można było scrollować po stronie. Ponadto jest flexem z wycentrowanym w środku obrazkiem. Style obrazka to właściwości na potrzeby animacji, którą będziemy manipulować w kodzie. W zamierzeniu klasa transitioned będzie przełączona w momencie kiedy obrazek znajdzie się we viewporcie.

Przejdźmy do kodu js (a raczej ts). Zacznijmy od zdefiniowania nowego obiektu observera:

// index.ts
const observer = new IntersectionObserver(callback, {
  rootMargin: '150px',
  threshold: 1.0,
});

Definiujemy go za pomocą konstruktora new IntersectionObserver, a wewnątrz jego argumentów podajemy jeszcze nie zdefiniowany callback, który za moment sobie napiszemy oraz obiekt options. options przyjmuje trzy opcjonalne klucze: rootMargin to string, przyjmujący odległość właściwego elementu od krawędzie viewportu. W powyższym przypadku observer przechwyci zdarzenie w momencie kiedy górna bądź dolna krawędź ekranu przeglądarki znajdzie się w odległości 150px. threshold mówi, w jakim stopniu element powinien znajdować się we viewporcie, żeby zdarzenie doszło do skutku. Wartość jest alfanumeryczna, przyjmująca zakres liczb zmiennoprzecinkowych od 0 do 1.0. W momencie podania wartości 0.5 zdarzenie odpali się nawet kiedy element będzie tylko w połowie widoczny (oczywiście uwzględniając rootMargin). Trzecim i ostatnim kluczem jest root. Możemy tutaj wysłać root container służący za viewport. Domyślnie jest to nasz zwyczajny viewport, więc prawdopodobnie w 99% przypadkach zostawimy to as default. Zdefiniujmy teraz wcześniej wspomniany callback:

const callback = (
  entries: IntersectionObserverEntry[],
  observer: IntersectionObserver
) => {
  console.log(entries);
};

Przyjmuje dwa argumenty: entries oraz observer. Ten drugi to obiekt observera, na którym zachodzi zdarzenie. Pierwszy natomiast to wszystkie przechwycone zdarzenia, z którego możemy wyciągnąć następujące wartości:

zdjęcie tematyczne
kluczowym jest tutaj isIntersecting oraz target

Otrzymujemy obiekt target, którego observer przechwycił we viewporcie. Daje nam to możliwość wykonania następującego tricku:

const callback = (
  entries: IntersectionObserverEntry[],
  observer: IntersectionObserver
) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;

    entry.target.classList.toggle('transitioned');
    if (image) observer.unobserve(image);
  });
};

Tym samym, w momencie kiedy nasz obrazek znajdzie się we viewporcie, wykona się animacja. Tak też działa zapewne masa bilbliotek do animacji, takich jak GSAP. Ze zdarzenia wyciągamy target i przełączamy jego klasę. Zauważ, że umieściłem tutaj prosty warunek, który polega na sprawdzeniu, czy element jest właśnie przechwytywany we viewporcie. Jeśli nie, to działanie funkcji się przerwie. Ma to zapobiec temu, że w momencie kiedy strona się inicjalnie ładuje i elementy, które są obserwowane się renderują, to animacja się nie wykona. Byłoby to bez sensu. Podczas, gdy animacja wykona się i element zostanie przechwycony, za pomocą funkcji unobserve "odoebserwujemy" nasz element DOM. Końcowy kod będzie wyglądał tak:

const image = document.querySelector('.image');

const callback = (
  entries: IntersectionObserverEntry[],
  observer: IntersectionObserver
) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;

    entry.target.classList.toggle('transitioned');
    if (image) observer.unobserve(image);
  });
};

const observer = new IntersectionObserver(callback, {
  rootMargin: '150px',
  threshold: 1.0,
});

if (image) observer.observe(image);     // w tym miejscu jeszcze inicjujemy obserwowanie elementu

Rezultat prezentuje się następująco:

Jak widzisz, nie wymagało zbyt dużej ilości pracy, by w czystym JavaScripcie (aczkolwiek użyłem tutaj TS) napisać coś, do czego zazwyczaj używamy ogromnych bibliotek do animacji. Czasem warto jest coś zrobić na własną rękę, szczególnie kiedy to coś będzie wykorzystane w małym stopniu.

© Damian Kalka 2021
Wszelkie prawa zastrzeżone