Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM
Wydajność i optymalizacja obsługi wydarzeń
Jak zapewnić wysoką wydajność przy intensywnym użyciu wydarzeń.
"Park z tysiącem czujników działa tylko wtedy, gdy żaden z nich nie blokuje głównej linii łączności," mówi Ray Arnold. W przeglądarce tą linią jest główny wątek. Ten sam wątek wykonuje Twój JavaScript i obsługuje przewijanie, więc ciężki listener potrafi sprawić, że strona na telefonie przewija się skokami. W tej lekcji zbieramy narzędzia, które odciążają ten wątek.
Optymalizacje wydajności
Pierwsze narzędzie to listener pasywny. Opcja { passive: true } to obietnica złożona przeglądarce: ten listener nigdy nie wywoła preventDefault().
1// Passive event listeners dla lepszej wydajności scroll
2window.addEventListener('scroll', handleScroll, { passive: true });
3window.addEventListener('touchstart', handleTouch, { passive: true });Przy wydarzeniach, które mogą zatrzymać przewijanie, czyli touchstart, touchmove i wheel, przeglądarka normalnie musiałaby czekać, aż listener skończy pracę, bo może on zablokować przewijanie. Po takiej obietnicy przewijanie rusza od razu, bez czekania na listener. Jeśli pasywny listener mimo to wywoła preventDefault(), przeglądarka zignoruje to wywołanie. Przy samym scroll flaga niczego nie przyspiesza, bo tego wydarzenia i tak nie da się anulować, ale nie szkodzi i jasno opisuje intencję.
Obiekt opcji zna więcej flag. Każda domyślnie ma wartość false i można je łączyć w jednym obiekcie:
1// Event listener tylko raz
2button.addEventListener('click', handleClick, { once: true });
3
4// Capture phase
5button.addEventListener('click', handleClick, { capture: true });
6
7// Kombinacja opcji
8element.addEventListener('scroll', handleScroll, {
9 passive: true,
10 capture: false,
11 once: false
12});Opcja once usuwa listener po pierwszym wywołaniu, więc nie trzeba pamiętać o sprzątaniu. Opcja capture przenosi listener do fazy przechwytywania, którą znasz z lekcji o propagacji. Sama w sobie nie przyspiesza kodu, ale należy do tego samego obiektu opcji. Przeglądarki traktują touchstart, touchmove i wheel na window, document, document.documentElement i body jako pasywne nawet bez tej opcji. Dlatego wolę zawsze wpisać passive: true jawnie.
Intersection Observer zamiast scroll
Klasyczny sposób wykrywania, czy element pojawił się na ekranie, to listener scroll, który przy każdym ruchu mierzy położenie elementu. IntersectionObserver odwraca ten układ: przeglądarka sama powiadamia Cię, gdy widoczność elementu się zmieni.
1// Intersection Observer zamiast scroll events
2const observer = new IntersectionObserver((entries) => {
3 entries.forEach(entry => {
4 if (entry.isIntersecting) {
5 // Element wszedł w viewport
6 loadLazyContent(entry.target);
7 }
8 });
9}, {
10 rootMargin: '100px' // Trigger 100px przed wejściem do viewport
11});
12
13// Obserwuj elementy
14document.querySelectorAll('.lazy-load').forEach(el => {
15 observer.observe(el);
16});Callback dostaje tablicę entries, po jednym wpisie na każdy element, którego widoczność się zmieniła. Właściwość isIntersecting mówi, czy element jest teraz w obszarze widoku, czyli w viewport. Opcja rootMargin: '100px' poszerza ten obszar o 100 pikseli, więc treść zaczyna się ładować, zanim gość do niej dojedzie. Jeden obserwator może pilnować dowolnej liczby elementów, a Twój callback nie uruchamia się między zmianami. Na tym opiera się leniwe ładowanie obrazów: loadLazyContent() może przepisać adres z atrybutu data-src do src, a potem wywołać observer.unobserve(), bo załadowany obraz nie wymaga dalszej obserwacji.
ResizeObserver zamiast resize
Wydarzenie resize na window mówi tylko o zmianie rozmiaru okna. Tymczasem panel może się zwęzić, bo obok rozwinął się pasek boczny, choć okno zostało takie samo.
1// ResizeObserver zamiast resize events
2const resizeObserver = new ResizeObserver(entries => {
3 entries.forEach(entry => {
4 const { width, height } = entry.contentRect;
5 updateElementLayout(entry.target, width, height);
6 });
7});
8
9resizeObserver.observe(document.getElementById('responsive-element'));ResizeObserver zgłasza zmianę rozmiaru konkretnego elementu, niezależnie od jej przyczyny. Z contentRect odczytujemy szerokość i wysokość obszaru treści, a układ dopasowujemy tylko wtedy, gdy naprawdę się zmienił. Oba obserwatory omówimy dokładniej w lekcji o zaawansowanym DOM API.
Kolejność decyzji
Moja rada, gdy strona zwalnia przez wydarzenia: najpierw zapytaj, czy listener scroll jest w ogóle potrzebny, bo często zastąpi go obserwator. Jeśli jest potrzebny, ogranicz go throttlingiem. Listenerom dotyku i kółka myszy dodaj passive: true. Zamiast setek listenerów na kartach użyj delegacji. Każdy krok zmniejsza pracę głównego wątku.
W piaskownicy poniżej zobaczysz pasywne listenery i Intersection Observer w działaniu. W kolejnej lekcji zobaczysz, jak wydarzenia porządkują architekturę całej aplikacji dzięki wzorcowi Event Bus.
Pamiętaj: najszybszy czujnik to taki, który nie blokuje linii łączności i odzywa się tylko wtedy, gdy naprawdę coś się dzieje.
Kod do tej lekcji: index.html
1<!DOCTYPE html>
2<html lang="pl">
3<head>
4 <meta charset="UTF-8">
5 <title>Performance i Optymalizacja</title>
6 <style>
7 body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
8 .demo { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; }
9 .scroll-box { height: 200px; overflow-y: auto; background: rgba(255,255,255,0.05); padding: 15px; border-radius: 6px; }
10 .stat { display: inline-block; margin: 10px; padding: 10px 20px; background: rgba(52, 152, 219, 0.2); border-radius: 6px; }
11 .dino-item { padding: 20px; margin: 10px 0; background: rgba(255,255,255,0.05); border-radius: 6px; }
12 </style>
13</head>
14<body>
15 <h1>Performance & Optymalizacja</h1>
16
17 <div class="demo">
18 <h3>Passive Event Listeners</h3>
19 <div class="scroll-box" id="passive-scroll">
20 <div style="height: 1000px;">
21 <p>Scroll me! (Passive listener = Better performance)</p>
22 <p style="margin-top: 200px;">Continue scrolling...</p>
23 <p style="margin-top: 200px;">Almost there...</p>
24 <p style="margin-top: 200px;">Good job!</p>
25 </div>
26 </div>
27 <div class="stat" id="scroll-count">Scrolls: 0</div>
28 </div>
29
30 <div class="demo">
31 <h3>Intersection Observer</h3>
32 <div class="scroll-box" id="observer-container">
33 <div class="dino-item" data-dino="T-Rex">T-Rex (not visible)</div>
34 <div class="dino-item" data-dino="Velociraptor">Velociraptor (not visible)</div>
35 <div class="dino-item" data-dino="Triceratops">Triceratops (not visible)</div>
36 <div class="dino-item" data-dino="Brachiosaurus">Brachiosaurus (not visible)</div>
37 </div>
38 <div class="stat" id="observer-count">Loaded: 0</div>
39 </div>
40
41 <script>
42 console.log('Performance optimization demo');
43
44 // PASSIVE EVENT LISTENERS
45 let scrollCount = 0;
46 const passiveScroll = document.getElementById('passive-scroll');
47
48 passiveScroll.addEventListener('scroll', () => {
49 scrollCount++;
50 document.getElementById('scroll-count').textContent = `Scrolls: ${scrollCount}`;
51 }, { passive: true }); // Passive dla lepszej wydajności
52
53 // INTERSECTION OBSERVER (zamiast scroll events)
54 let loadedCount = 0;
55 const observer = new IntersectionObserver((entries) => {
56 entries.forEach(entry => {
57 if (entry.isIntersecting) {
58 const dinoName = entry.target.dataset.dino;
59 entry.target.textContent = `${entry.target.textContent.split('(')[0]}(LOADED!)`;
60 entry.target.style.background = 'rgba(46, 204, 113, 0.2)';
61 loadedCount++;
62 document.getElementById('observer-count').textContent = `Loaded: ${loadedCount}`;
63 console.log(`${dinoName} entered viewport`);
64 observer.unobserve(entry.target); // Stop observing po załadowaniu
65 }
66 });
67 }, {
68 threshold: 0.5 // Trigger gdy 50% elementu jest widoczne
69 });
70
71 // Obserwuj wszystkie elementy dinozaurów
72 document.querySelectorAll('.dino-item').forEach(item => {
73 observer.observe(item);
74 });
75
76 // ONCE OPTION - event listener tylko raz
77 document.body.addEventListener('click', () => {
78 console.log('This fires only ONCE');
79 alert('Ten alert pojawi się tylko raz!');
80 }, { once: true });
81
82 // RESIZE OBSERVER (zamiast resize events)
83 const resizeObserver = new ResizeObserver(entries => {
84 entries.forEach(entry => {
85 const { width, height } = entry.contentRect;
86 console.log(`Container resized: ${Math.round(width)}x${Math.round(height)}`);
87 });
88 });
89
90 resizeObserver.observe(document.getElementById('observer-container'));
91
92 console.log('All performance optimizations active');
93 console.log('Using: Passive listeners, Intersection Observer, Resize Observer');
94 </script>
95</body>
96</html>Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza opcja { passive: true } w addEventListener?
2. Do czego służy IntersectionObserver?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
W pliku index.js są dwie funkcje. setupLazyLoading(images) ma obserwować obrazki i dopiero wtedy, gdy obrazek wejdzie w widok, przepisać adres z data-src do src. setupScrollProgress(target, onProgress) dodaje listener scroll, który przekazuje scrollTop do onProgress. Uzupełnij luki: ___BLANK1___ to konstruktor obserwatora widoczności, ___BLANK2___ to metoda obserwatora, która kończy obserwację załadowanego obrazka, a ___BLANK3___ to opcja addEventListener, za pomocą której listener obiecuje, że nie wywoła preventDefault().
- Klikanie w kolejności
Ułóż fragmenty kodu, aby uzyskać obiekt opcji { passive: true }:
- Klikanie w kolejności
Ułóż składnię pasywnego event listenera: