Kurs JavaScript i React · Moduł 5: useEffect i własne hooki

Optymalizacja wydajności z useEffect

7 min czytania
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:

  1. Zbyt częste wykonywanie efektów - gdy zależności zmieniają się często lub są nieprawidłowo zdefiniowane
  2. Kosztowne operacje - gdy wewnątrz efektu wykonujemy złożone obliczenia lub manipulacje DOM
  3. Kaskadowe aktualizacje - gdy jeden efekt powoduje zmiany stanu, które wyzwalają kolejne efekty
  4. 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: useLayoutEffect blokuje renderowanie wizualne, więc powinien być używany oszczędnie i tylko gdy absolutnie konieczne.

Podsumowanie: Najlepsze praktyki optymalizacji useEffect

  1. Minimalizuj zależności - używaj tylko niezbędnych wartości w tablicy zależności
  2. Separuj efekty - zamiast jednego złożonego efektu, używaj kilku prostszych
  3. Memoryzuj zależności - używaj useMemo i useCallback dla obiektów i funkcji
  4. Używaj debouncing/throttling - dla szybko zmieniających się wartości
  5. Anuluj przestarzałe operacje asynchroniczne - unikaj wyścigów i wycieków pamięci
  6. Twórz własne hooki - enkapsuluj złożoną logikę w reużywalnych hookach
  7. Analizuj wzorce renderowania - używaj React DevTools do wykrywania nadmiarowych renderowań
  8. 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. 1. Co powinna zwracać funkcja cleanup w useEffect?

  2. 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

Przydatne artykuły