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.
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ądarceIntersectionObserver 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 obserwacjeNajpopularniejsze 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]");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}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);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}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}Dr. Grant podsumowuje: "Obserwatory to jak nasz system czujników w parku - nie musimy ciągle sprawdzać każdej kamery ręcznie:"
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.