Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Web APIs: Intersection Observer i Resize Observer

Dr. Alan Grant stoi przed panoramicznym oknem centrum kontroli Parku Jurajskiego. "Nasze monitory wyświetlają setki elementów jednocześnie - obrazy z kamer, wykresy, mapy sektorów," mówi. "Ale ładowanie wszystkiego naraz zabija wydajność. Potrzebujemy sposobu, aby reagować na to, co użytkownik faktycznie widzi i jak zmienia się układ strony." Odpowiedzią są Intersection Observer i Resize Observer - nowoczesne Web APIs do obserwacji elementów DOM.

Problem: Ręczne śledzenie widoczności

Tradycyjne podejście do sprawdzania, czy element jest widoczny na ekranie, wymaga nasłuchiwania zdarzenia

scroll
i ręcznego obliczania pozycji:

1// Stare podejście - wolne i skomplikowane
2window.addEventListener("scroll", () => {
3  const images = document.querySelectorAll("img[data-src]");
4
5  images.forEach(img => {
6    const rect = img.getBoundingClientRect();
7    const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
8
9    if (isVisible) {
10      img.src = img.dataset.src; // Załaduj obrazek
11    }
12  });
13});
14// Problem: scroll event odpala się dziesiątki razy na sekundę!
15// Każde wywołanie getBoundingClientRect() wymusza reflow w przeglądarce

Intersection Observer - obserwacja widoczności

IntersectionObserver
pozwala asynchronicznie obserwować, kiedy element wchodzi lub wychodzi z widocznego obszaru (viewport) lub innego elementu:

1// Tworzenie obserwatora
2const observer = new IntersectionObserver((entries) => {
3  entries.forEach(entry => {
4    // entry.isIntersecting - czy element jest widoczny
5    // entry.intersectionRatio - jaki procent elementu jest widoczny (0-1)
6    // entry.target - obserwowany element DOM
7
8    if (entry.isIntersecting) {
9      console.log(`Element ${entry.target.id} jest teraz widoczny!`);
10      console.log(`Widoczność: ${(entry.intersectionRatio * 100).toFixed(0)}%`);
11    }
12  });
13}, {
14  // Opcje konfiguracyjne
15  root: null,        // null = viewport przeglądarki
16  rootMargin: "0px", // margines wokół root (jak CSS margin)
17  threshold: 0.5     // callback gdy 50% elementu jest widoczne
18});
19
20// Rozpoczęcie obserwacji
21const dinoCard = document.querySelector("#dino-card");
22observer.observe(dinoCard);
23
24// Zatrzymanie obserwacji
25// observer.unobserve(dinoCard);
26// observer.disconnect(); // Zatrzymuje wszystkie obserwacje

Lazy loading obrazów

Najpopularniejsze zastosowanie Intersection Observer to lazy loading - ładowanie obrazów dopiero, gdy użytkownik do nich przewinie:

1class LazyImageLoader {
2  constructor(options = {}) {
3    this.observer = new IntersectionObserver(
4      (entries) => this.handleIntersection(entries),
5      {
6        rootMargin: options.rootMargin || "200px", // Ładuj 200px przed widokiem
7        threshold: 0
8      }
9    );
10  }
11
12  handleIntersection(entries) {
13    entries.forEach(entry => {
14      if (entry.isIntersecting) {
15        const img = entry.target;
16
17        // Zamień placeholder na prawdziwy obrazek
18        img.src = img.dataset.src;
19        img.classList.add("loaded");
20
21        // Przestań obserwować załadowany obrazek
22        this.observer.unobserve(img);
23
24        console.log(`Załadowano obraz: ${img.dataset.src}`);
25      }
26    });
27  }
28
29  observe(selector) {
30    const images = document.querySelectorAll(selector);
31    images.forEach(img => this.observer.observe(img));
32    console.log(`Obserwuję ${images.length} obrazów`);
33  }
34
35  destroy() {
36    this.observer.disconnect();
37  }
38}
39
40// Użycie
41const loader = new LazyImageLoader({ rootMargin: "300px" });
42loader.observe("img[data-src]");

Infinite scroll

Intersection Observer świetnie nadaje się do implementacji nieskończonego przewijania:

1class InfiniteScroll {
2  constructor(containerSelector, loadMore) {
3    this.container = document.querySelector(containerSelector);
4    this.loadMore = loadMore;
5    this.isLoading = false;
6    this.page = 1;
7
8    // Tworzymy element "strażnika" na końcu listy
9    this.sentinel = document.createElement("div");
10    this.sentinel.className = "scroll-sentinel";
11    this.container.appendChild(this.sentinel);
12
13    // Obserwujemy strażnika
14    this.observer = new IntersectionObserver(
15      (entries) => {
16        if (entries[0].isIntersecting && !this.isLoading) {
17          this.loadNextPage();
18        }
19      },
20      { rootMargin: "100px" }
21    );
22
23    this.observer.observe(this.sentinel);
24  }
25
26  async loadNextPage() {
27    this.isLoading = true;
28    console.log(`Ładuję stronę ${this.page}...`);
29
30    try {
31      const items = await this.loadMore(this.page);
32
33      items.forEach(item => {
34        const element = document.createElement("div");
35        element.className = "dino-card";
36        element.textContent = `${item.name} (${item.species})`;
37        // Wstawiamy PRZED strażnikiem
38        this.container.insertBefore(element, this.sentinel);
39      });
40
41      this.page++;
42    } catch (error) {
43      console.error("Błąd ładowania:", error);
44    } finally {
45      this.isLoading = false;
46    }
47  }
48}

Resize Observer - obserwacja zmian rozmiaru

ResizeObserver
powiadamia, gdy element zmieni swoje wymiary. Jest nieoceniony dla responsywnych komponentów:

1// Tworzenie Resize Observer
2const resizeObserver = new ResizeObserver((entries) => {
3  entries.forEach(entry => {
4    const { width, height } = entry.contentRect;
5    console.log(`Element ${entry.target.id} zmienił rozmiar: ${width}x${height}`);
6  });
7});
8
9// Obserwacja elementu
10const mapContainer = document.querySelector("#park-map");
11resizeObserver.observe(mapContainer);

Responsywny komponent z Resize Observer

1class ResponsiveDinoGrid {
2  constructor(containerSelector) {
3    this.container = document.querySelector(containerSelector);
4    this.currentLayout = null;
5
6    this.observer = new ResizeObserver((entries) => {
7      const entry = entries[0];
8      const width = entry.contentRect.width;
9      this.updateLayout(width);
10    });
11
12    this.observer.observe(this.container);
13  }
14
15  updateLayout(width) {
16    let newLayout;
17
18    if (width < 400) {
19      newLayout = "single-column";
20      this.container.style.gridTemplateColumns = "1fr";
21    } else if (width < 800) {
22      newLayout = "two-columns";
23      this.container.style.gridTemplateColumns = "1fr 1fr";
24    } else {
25      newLayout = "multi-column";
26      this.container.style.gridTemplateColumns = "repeat(auto-fill, minmax(250px, 1fr))";
27    }
28
29    if (newLayout !== this.currentLayout) {
30      this.currentLayout = newLayout;
31      console.log(`Layout zmieniony na: ${newLayout} (szerokość: ${width}px)`);
32    }
33  }
34
35  destroy() {
36    this.observer.disconnect();
37  }
38}

Łączenie obu Observerów

1// System panelu monitoringu Parku Jurajskiego
2class MonitoringPanel {
3  constructor(panelSelector) {
4    this.panel = document.querySelector(panelSelector);
5    this.cameras = [];
6
7    // Intersection Observer - włączaj/wyłączaj kamery w zależności od widoczności
8    this.visibilityObserver = new IntersectionObserver(
9      (entries) => this.handleVisibility(entries),
10      { threshold: 0.1 }
11    );
12
13    // Resize Observer - dostosuj jakość obrazu do rozmiaru panelu
14    this.resizeObserver = new ResizeObserver(
15      (entries) => this.handleResize(entries)
16    );
17  }
18
19  addCamera(cameraElement) {
20    this.cameras.push(cameraElement);
21    this.visibilityObserver.observe(cameraElement);
22    this.resizeObserver.observe(cameraElement);
23  }
24
25  handleVisibility(entries) {
26    entries.forEach(entry => {
27      const cameraId = entry.target.dataset.cameraId;
28
29      if (entry.isIntersecting) {
30        console.log(`Kamera ${cameraId}: WŁĄCZONA (widoczna)`);
31        entry.target.classList.add("active");
32      } else {
33        console.log(`Kamera ${cameraId}: WSTRZYMANA (poza widokiem)`);
34        entry.target.classList.remove("active");
35      }
36    });
37  }
38
39  handleResize(entries) {
40    entries.forEach(entry => {
41      const { width } = entry.contentRect;
42      const cameraId = entry.target.dataset.cameraId;
43
44      let quality;
45      if (width < 200) quality = "low";
46      else if (width < 500) quality = "medium";
47      else quality = "high";
48
49      entry.target.dataset.quality = quality;
50      console.log(`Kamera ${cameraId}: jakość = ${quality} (szer: ${width}px)`);
51    });
52  }
53
54  destroy() {
55    this.visibilityObserver.disconnect();
56    this.resizeObserver.disconnect();
57  }
58}

Podsumowanie

Dr. Grant podsumowuje: "Obserwatory to jak nasz system czujników w parku - nie musimy ciągle sprawdzać każdej kamery ręcznie:"

  1. IntersectionObserver - reaguje, gdy element wchodzi/wychodzi z widocznego obszaru
  2. Lazy loading - ładuj obrazy/treści dopiero gdy użytkownik do nich przewinie
  3. Infinite scroll - automatycznie doładowuj dane przy przewijaniu
  4. ResizeObserver - reaguje na zmianę rozmiaru elementów DOM
  5. Responsywne komponenty - dostosowuj layout bez media queries

Oba API są asynchroniczne i znacznie wydajniejsze niż ręczne nasłuchiwanie

scroll
czy
resize
. Przeglądarka optymalizuje wywołania callbacków, dzięki czemu nie blokują one głównego wątku.

Przejdź do CodeWorlds