Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Optymalizacja wydajności z useEffect
W tej lekcji7
W kosmicznej podróży przez React, wydajność jest krytycznie ważna - podobnie jak zarządzanie zasobami na statku kosmicznym. Nieoptymalne wykorzystanie efektów ubocznych może prowadzić do nadmiernej liczby renderowań, marnotrawstwa zasobów i wolnej reakcji aplikacji. W tym module nauczymy się, jak optymalizować działanie useEffect, aby nasze aplikacje działały płynnie i efektywnie.
Problemy wydajnościowe z useEffect
Hook useEffect jest potężnym narzędziem, ale niewłaściwie używany może powodować różne problemy:
- Zbyt częste wykonywanie efektów - gdy zależności zmieniają się często lub są nieprawidłowo zdefiniowane
- Kosztowne operacje - gdy wewnątrz efektu wykonujemy złożone obliczenia lub manipulacje DOM
- Kaskadowe aktualizacje - gdy jeden efekt powoduje zmiany stanu, które wyzwalają kolejne efekty
- Niepotrzebne renderowania - gdy efekty aktualizują stan, mimo że dane się faktycznie nie zmieniły
Optymalizacja tablicy zależności
Tablica zależności w useEffect jest kluczem do optymalizacji - określa kiedy efekt powinien być ponownie wykonany.
Błąd 1: Zbyt wiele zależności
1// Problematyczny kod
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // Ten efekt będzie uruchamiany zbyt często
7 useEffect(() => {
8 console.log(`Śledzenie astronauty: ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team, position, status]); // Zbędne zależności: position i status
13
14 return <div>Pozycja lidera: {position.x}, {position.y}</div>;
15}Rozwiązanie: Używaj tylko niezbędnych zależności
1// Poprawiony kod
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // Efekt używa tylko potrzebnych zależności
7 useEffect(() => {
8 console.log(`Śledzenie astronauty: ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team.leader]); // Tylko potrzebna zależność
13
14 return <div>Pozycja lidera: {position.x}, {position.y}</div>;
15}Technika: Memoryzacja obiektów i funkcji
Obiekty i funkcje tworzone podczas renderowania są zawsze "nowe" z perspektywy porównywania zależności. Możemy to zoptymalizować za pomocą hooków useMemo i useCallback:
1function SpaceshipDashboard() {
2 // Bez optymalizacji - obiekt config jest tworzony na nowo przy każdym renderze
3 const config = { speed: 10, fuel: 100 };
4
5 // Z optymalizacją - obiekt config jest zapamiętany i reużyty
6 const memoizedConfig = useMemo(() => ({ speed: 10, fuel: 100 }), []);
7
8 // Bez optymalizacji - funkcja handleData jest tworzona na nowo przy każdym renderze
9 const handleData = (data) => console.log(data);
10
11 // Z optymalizacją - funkcja handleData jest zapamiętana i reużyta
12 const memoizedHandleData = useCallback((data) => console.log(data), []);
13
14 useEffect(() => {
15 // Ten efekt będzie uruchamiany tylko raz
16 const subscription = spaceApi.subscribe(memoizedConfig, memoizedHandleData);
17 return () => subscription.unsubscribe();
18 }, [memoizedConfig, memoizedHandleData]); // Zależności są stabilne
19
20 return <div>Statek kosmiczny</div>;
21}Techniki optymalizacji kosztownych operacji
1. Debouncing i throttling
Gdy efekt reaguje na szybko zmieniające się wartości (np. input użytkownika), warto użyć debouncing lub throttling:
1function SearchAsteroids({ query }) {
2 const [results, setResults] = useState([]);
3 const [debouncedQuery, setDebouncedQuery] = useState(query);
4
5 // Debouncing zapytania - aktualizuje się nie częściej niż co 500ms
6 useEffect(() => {
7 const timeoutId = setTimeout(() => {
8 setDebouncedQuery(query);
9 }, 500);
10
11 return () => clearTimeout(timeoutId);
12 }, [query]);
13
14 // Efekt wyszukiwania wykonuje się tylko gdy debouncedQuery się zmieni
15 useEffect(() => {
16 if (debouncedQuery) {
17 searchAsteroids(debouncedQuery).then(setResults);
18 }
19 }, [debouncedQuery]);
20
21 return (
22 <div>
23 <h2>Wyniki wyszukiwania</h2>
24 <ul>
25 {results.map(asteroid => (
26 <li key={asteroid.id}>{asteroid.name}</li>
27 ))}
28 </ul>
29 </div>
30 );
31}2. Unikanie niepotrzebnych aktualizacji stanu
1function SpaceStation({ stationId }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 let isMounted = true;
6
7 fetchStationData(stationId).then(newData => {
8 // Aktualizuj tylko jeśli komponent jest wciąż zamontowany
9 if (isMounted) {
10 // Aktualizuj tylko jeśli dane faktycznie się zmieniły
11 if (JSON.stringify(data) !== JSON.stringify(newData)) {
12 setData(newData);
13 }
14 }
15 });
16
17 return () => {
18 isMounted = false;
19 };
20 }, [stationId]);
21
22 return data ? <StationDisplay data={data} /> : <Loading />;
23}Optymalizacja efektów z danymi asynchronicznymi
Problem: Wyścigi (race conditions)
Gdy efekt wykonuje asynchroniczne operacje, może dojść do "wyścigów" - sytuacji, gdzie nowszy request zakończy się przed starszym:
1// Problematyczny kod - podatny na wyścigi
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 fetchAsteroidDetails(asteroidId).then(response => {
7 setDetails(response); // Możliwy wyścig, jeśli asteroidId zmieni się szybko
8 });
9 }, [asteroidId]);
10
11 return <div>{details?.name}</div>;
12}Rozwiązanie: Anulowanie przestarzałych requestów
1// Poprawiony kod - zabezpieczony przed wyścigami
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 let isCurrent = true;
7
8 fetchAsteroidDetails(asteroidId).then(response => {
9 // Aktualizuj stan tylko jeśli to wciąż aktualny request
10 if (isCurrent) {
11 setDetails(response);
12 }
13 });
14
15 return () => {
16 isCurrent = false; // Oznacz, że ten efekt nie jest już aktualny
17 };
18 }, [asteroidId]);
19
20 return <div>{details?.name}</div>;
21}Technika zaawansowana: AbortController
Dla API, które wspierają AbortController (np. fetch), możemy faktycznie anulować trwające requesty:
1function PlanetScanner({ sector }) {
2 const [planets, setPlanets] = useState([]);
3 const [loading, setLoading] = useState(false);
4
5 useEffect(() => {
6 setLoading(true);
7
8 // Tworzenie kontrolera anulowania
9 const controller = new AbortController();
10 const signal = controller.signal;
11
12 fetch(`https://api.space/planets?sector=${sector}`, { signal })
13 .then(response => response.json())
14 .then(data => {
15 setPlanets(data);
16 setLoading(false);
17 })
18 .catch(error => {
19 if (error.name !== 'AbortError') {
20 console.error('Błąd:', error);
21 setLoading(false);
22 }
23 });
24
25 // Funkcja czyszcząca anuluje request, jeśli efekt zostanie przerwany
26 return () => {
27 controller.abort();
28 };
29 }, [sector]);
30
31 return (
32 <div>
33 {loading ? <Loading /> : (
34 <PlanetsList planets={planets} />
35 )}
36 </div>
37 );
38}Separacja logiki do własnych hooków
Jednym z najlepszych sposobów optymalizacji jest tworzenie własnych hooków, które enkapsulują złożoną logikę i mogą być reużyte między komponentami:
1// Własny hook do pobierania i zarządzania danymi
2function usePlanetData(planetId) {
3 const [data, setData] = useState(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState(null);
6
7 useEffect(() => {
8 let isMounted = true;
9 setLoading(true);
10
11 fetchPlanetData(planetId)
12 .then(result => {
13 if (isMounted) {
14 setData(result);
15 setLoading(false);
16 }
17 })
18 .catch(err => {
19 if (isMounted) {
20 setError(err);
21 setLoading(false);
22 }
23 });
24
25 return () => {
26 isMounted = false;
27 };
28 }, [planetId]);
29
30 return { data, loading, error };
31}
32
33// Korzystanie z własnego hooka
34function PlanetProfile({ planetId }) {
35 const { data, loading, error } = usePlanetData(planetId);
36
37 if (loading) return <LoadingIndicator />;
38 if (error) return <ErrorMessage error={error} />;
39
40 return (
41 <div>
42 <h1>{data.name}</h1>
43 <p>Średnica: {data.diameter} km</p>
44 <p>Klimat: {data.climate}</p>
45 </div>
46 );
47}Używanie mechanizmu layoutEffect
W rzadkich przypadkach, gdy potrzebujesz wykonać pomiary DOM lub synchronizować się z układem strony przed renderowaniem, możesz użyć useLayoutEffect zamiast useEffect:
1function CockpitInterface() {
2 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
3 const cockpitRef = useRef(null);
4
5 // useLayoutEffect uruchamia się synchronicznie po wszystkich mutacjach DOM,
6 // ale przed renderowaniem na ekranie
7 useLayoutEffect(() => {
8 if (cockpitRef.current) {
9 const { width, height } = cockpitRef.current.getBoundingClientRect();
10 setDimensions({ width, height });
11 }
12 }, []);
13
14 return (
15 <div ref={cockpitRef} className="cockpit">
16 <ControlPanel width={dimensions.width} height={dimensions.height} />
17 </div>
18 );
19}Uwaga:
useLayoutEffectblokuje renderowanie wizualne, więc powinien być używany oszczędnie i tylko gdy absolutnie konieczne.
Podsumowanie: Najlepsze praktyki optymalizacji useEffect
- Minimalizuj zależności - używaj tylko niezbędnych wartości w tablicy zależności
- Separuj efekty - zamiast jednego złożonego efektu, używaj kilku prostszych
- Memoryzuj zależności - używaj useMemo i useCallback dla obiektów i funkcji
- Używaj debouncing/throttling - dla szybko zmieniających się wartości
- Anuluj przestarzałe operacje asynchroniczne - unikaj wyścigów i wycieków pamięci
- Twórz własne hooki - enkapsuluj złożoną logikę w reużywalnych hookach
- Analizuj wzorce renderowania - używaj React DevTools do wykrywania nadmiarowych renderowań
- Unikaj nieskończonych pętli - zawsze upewnij się, że efekty nie powodują nieskończonych cykli aktualizacji
Stosując te techniki optymalizacji, twoja aplikacja React będzie działać efektywnie nawet przy złożonych operacjach i dużych zbiorach danych - podobnie jak dobrze zoptymalizowany statek kosmiczny może pokonywać ogromne odległości z minimalnym zużyciem zasobów.
Kod do tej lekcji: App.jsx
1import React, { useState, useEffect, useRef, useCallback } from 'react';
2
3// Demonstracja tablicy zależności
4function DependencyArrayDemo() {
5 const [count, setCount] = useState(0);
6 const [name, setName] = useState('');
7 const [logs, setLogs] = useState([]);
8
9 const addLog = (msg) => {
10 setLogs(prev => [...prev.slice(-4), `[${new Date().toLocaleTimeString()}] ${msg}`]);
11 };
12
13 // Effect bez zależności - uruchamia się przy każdym renderze
14 const effectRuns = useRef(0);
15 useEffect(() => {
16 // Bez setState: zmiana stanu tutaj wywołałaby kolejny render i nieskończoną pętlę
17 effectRuns.current += 1;
18 console.log(`Effect BEZ tablicy zależności (${effectRuns.current})`);
19 });
20
21 // Effect z pustą tablicą - tylko przy montowaniu
22 useEffect(() => {
23 addLog('Effect z [] - tylko przy mount');
24 }, []);
25
26 // Effect z zależnością od count
27 useEffect(() => {
28 addLog(`Effect z [count] - count = ${count}`);
29 }, [count]);
30
31 // Effect z zależnością od name
32 useEffect(() => {
33 if (name) addLog(`Effect z [name] - name = "${name}"`);
34 }, [name]);
35
36 return (
37 <div className="demo-card">
38 <h2>Tablica zależności</h2>
39 <div className="inputs">
40 <button onClick={() => setCount(c => c + 1)}>
41 Count: {count}
42 </button>
43 <input
44 value={name}
45 onChange={(e) => setName(e.target.value)}
46 placeholder="Wpisz imię..."
47 />
48 </div>
49 <div className="logs">
50 {logs.map((log, i) => <div key={i}>{log}</div>)}
51 </div>
52 </div>
53 );
54}
55
56// Optymalizacja fetch requestów
57function OptimizedFetchDemo() {
58 const [searchTerm, setSearchTerm] = useState('');
59 const [results, setResults] = useState([]);
60 const [loading, setLoading] = useState(false);
61 const [fetchCount, setFetchCount] = useState(0);
62 const abortControllerRef = useRef(null);
63
64 // Debounced search z cleanup
65 useEffect(() => {
66 // Anuluj poprzednie żądanie
67 if (abortControllerRef.current) {
68 abortControllerRef.current.abort();
69 }
70
71 if (!searchTerm.trim()) {
72 setResults([]);
73 return;
74 }
75
76 // Debounce - czekaj 500ms przed wyszukaniem
77 const timeoutId = setTimeout(() => {
78 abortControllerRef.current = new AbortController();
79
80 setLoading(true);
81 setFetchCount(c => c + 1);
82
83 // Symulacja API call
84 setTimeout(() => {
85 const mockResults = [
86 `Planeta: ${searchTerm}-Prime`,
87 `Gwiazda: ${searchTerm}-Alpha`,
88 `Mgławica: ${searchTerm}-Nebula`,
89 ];
90 setResults(mockResults);
91 setLoading(false);
92 }, 500);
93 }, 500);
94
95 // Cleanup - anuluj timeout i żądanie
96 return () => {
97 clearTimeout(timeoutId);
98 if (abortControllerRef.current) {
99 abortControllerRef.current.abort();
100 }
101 };
102 }, [searchTerm]);
103
104 return (
105 <div className="demo-card">
106 <h2>Optymalizacja Fetch</h2>
107 <div className="fetch-counter">
108 Wykonanych zapytań: <span>{fetchCount}</span>
109 </div>
110 <input
111 value={searchTerm}
112 onChange={(e) => setSearchTerm(e.target.value)}
113 placeholder="Szukaj obiektów kosmicznych..."
114 />
115 <div className="results">
116 {loading ? (
117 <div className="loading">Wyszukiwanie...</div>
118 ) : results.length > 0 ? (
119 results.map((result, i) => (
120 <div key={i} className="result">{result}</div>
121 ))
122 ) : searchTerm ? (
123 <div className="empty">Wpisz, aby wyszukać</div>
124 ) : null}
125 </div>
126 <p className="hint">Debounce 500ms + AbortController</p>
127 </div>
128 );
129}
130
131// Unikanie nieskończonych pętli
132function InfiniteLoopPreventionDemo() {
133 const [data, setData] = useState({ count: 0 });
134 const renderCount = useRef(0);
135 const [safeMode, setSafeMode] = useState(true);
136
137 // Zliczanie renderów
138 renderCount.current += 1;
139
140 // NIEBEZPIECZNE - spowodowałoby nieskończoną pętlę
141 // useEffect(() => {
142 // setData({ count: data.count }); // Nowy obiekt = nowa referencja = re-render
143 // }, [data]);
144
145 // BEZPIECZNE - używamy callbacka
146 const incrementSafe = useCallback(() => {
147 setData(prev => ({ count: prev.count + 1 }));
148 }, []);
149
150 return (
151 <div className="demo-card">
152 <h2>Unikanie pętli</h2>
153 <div className="stats">
154 <div className="stat">
155 <span className="label">Count:</span>
156 <span className="value">{data.count}</span>
157 </div>
158 <div className="stat">
159 <span className="label">Rendery:</span>
160 <span className="value">{renderCount.current}</span>
161 </div>
162 </div>
163 <button onClick={incrementSafe}>
164 Bezpieczny increment (useCallback)
165 </button>
166 <p className="hint">
167 Używaj funkcji aktualizującej zamiast bezpośredniej referencji
168 </p>
169 </div>
170 );
171}
172
173// Optymalizacja z useRef zamiast state
174function UseRefOptimizationDemo() {
175 const [, forceRender] = useState(0);
176 const clickCountRef = useRef(0);
177 const renderCountRef = useRef(0);
178
179 renderCountRef.current++;
180
181 const handleClick = () => {
182 clickCountRef.current++;
183 // Nie powoduje re-renderu!
184 };
185
186 return (
187 <div className="demo-card">
188 <h2>useRef vs useState</h2>
189 <div className="comparison">
190 <div className="box">
191 <h3>useRef (bez re-renderu)</h3>
192 <p>Kliknięcia: {clickCountRef.current}</p>
193 <button onClick={handleClick}>Kliknij (useRef)</button>
194 </div>
195 <div className="box">
196 <h3>Rendery komponentu</h3>
197 <p>Liczba: {renderCountRef.current}</p>
198 <button onClick={() => forceRender(n => n + 1)}>
199 Wymuś render
200 </button>
201 </div>
202 </div>
203 <p className="hint">
204 useRef nie powoduje re-renderu przy zmianie wartości
205 </p>
206 </div>
207 );
208}
209
210function App() {
211 return (
212 <div className="app">
213 <h1>Optymalizacja useEffect</h1>
214 <div className="demos">
215 <DependencyArrayDemo />
216 <OptimizedFetchDemo />
217 <InfiniteLoopPreventionDemo />
218 <UseRefOptimizationDemo />
219 </div>
220 </div>
221 );
222}
223
224export default App;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 powinna zwracać funkcja cleanup w useEffect?
2. Dlaczego umieszczenie obiektu bezpośrednio w tablicy zależności useEffect może powodować nieskończoną pętlę?
To 2 z 5 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Ułóż kolejność kroków renderowania React z useEffect:
- Edytor kodu
Hook zarządzania klatkami animacji
- Edytor kodu
Implementacja wyszukiwania z debounce
- Edytor kodu
Stwórz komponent TemperatureMonitor używający useEffect do symulowania odczytów temperatury co 2 sekundy.
- Układanie w pionie
Ułóż ewolucję optymalizacji event handling:
- Edytor kodu
Stwórz komponent CommunicationChannel z useEffect, który ustanawia połączenie i czyści przy unmount.
- Klikanie w kolejności
Ułóż cykl życia useEffect:
- Edytor kodu
Stwórz EarthCommunication używający useEffect do fetch danych z API i obsługi loading/error states.
- Układanie w pionie
Ułóż ścieżkę opanowania useEffect:
- Edytor kodu
Stwórz ColonySettings z useEffect do synchronizacji state z localStorage (theme, language, etc.).
- Edytor kodu
Stwórz ShipDashboard z useState dla paliwa/temperatury/silnika, useEffect z interwałem co 2s, oraz przyciskami do sterowania systemami