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

Memoizacja - React.memo, useMemo i useCallback

6 min czytania
W tej lekcji8

W przestrzeni kosmicznej, zasoby są ograniczone i trzeba je mądrze wykorzystywać. Podobnie w świecie React, wydajność aplikacji ma kluczowe znaczenie. Memoizacja to technika optymalizacyjna, która pozwala na "zapamiętywanie" wyników kosztownych operacji i ponowne ich wykorzystanie, gdy wejściowe dane nie uległy zmianie - podobnie jak zapisywanie i wykorzystywanie obliczeń trajektorii kosmicznej zamiast wykonywania ich od nowa.

Czym jest memoizacja w React?

Memoizacja w React to mechanizm optymalizacji wydajności, który pozwala:

  • Unikać niepotrzebnych ponownych renderowań komponentów
  • Zapobiegać ponownemu wykonywaniu kosztownych obliczeń
  • Stabilizować referencje do funkcji i obiektów

React.memo - memoizacja komponentów

React.memo to komponent wyższego rzędu (HOC), który opakowuje komponent i zapobiega jego ponownemu renderowaniu, jeśli jego propsy nie uległy zmianie.

Podstawowe użycie React.memo

1import React from 'react';
2
3// Komponent bez memoizacji
4function PlanetInfo({ name, diameter, distance }) {
5  console.log(`Renderowanie informacji o planecie: ${name}`);
6  return (
7    <div className="planet-info">
8      <h2>{name}</h2>
9      <p>Średnica: {diameter} km</p>
10      <p>Odległość od Słońca: {distance} mln km</p>
11    </div>
12  );
13}
14
15// Ten sam komponent z memoizacją
16const MemoizedPlanetInfo = React.memo(PlanetInfo);
17
18// Komponent rodzic
19function SolarSystem() {
20  const [count, setCount] = useState(0);
21
22  return (
23    <div>
24      <button onClick={() => setCount(count + 1)}>
25        Licznik: {count}
26      </button>
27
28      {/* Ten komponent będzie renderowany ponownie tylko gdy jego propsy się zmienią */}
29      <MemoizedPlanetInfo
30        name="Mars"
31        diameter={6779}
32        distance={228}
33      />
34    </div>
35  );
36}

Zauważ, że w powyższym przykładzie, mimo że komponent nadrzędny SolarSystem renderuje się ponownie przy każdym kliknięciu przycisku, komponent MemoizedPlanetInfo nie będzie ponownie renderowany, ponieważ jego propsy pozostają niezmienione.

Niestandardowe porównanie propsów

Domyślnie React.memo porównuje propsy za pomocą płytkiego porównania (shallow comparison). Możesz jednak dostarczyć własną funkcję porównującą:

1const MemoizedPlanetInfo = React.memo(
2  PlanetInfo,
3  (prevProps, nextProps) => {
4    // Zwróć true, jeśli chcesz zapobiec ponownemu renderowaniu
5    // Zwróć false, jeśli chcesz ponownie wyrenderować komponent
6    return prevProps.name === nextProps.name &&
7           prevProps.diameter === nextProps.diameter;
8    // Zauważ, że ignorujemy zmianę distance - komponent nie wyrenderuje się ponownie
9    // kiedy zmieni się tylko ta właściwość
10  }
11);

useMemo - memoizacja wartości

Hook useMemo pozwala na zapamiętanie wyniku kosztownej operacji i ponowne użycie go, o ile zależności nie uległy zmianie.

Kiedy używać useMemo?

  1. Przy kosztownych obliczeniach
  2. Przy tworzeniu obiektów referencyjnych jako propsy dla komponentów potomnych
  3. Przy tworzeniu obszernych struktur danych
1import React, { useState, useMemo } from 'react';
2
3function AsteroidCalculator({ asteroids }) {
4  const [filter, setFilter] = useState('all');
5
6  // Kosztowne obliczenie - memoizowane dla lepszej wydajności
7  const dangerousAsteroids = useMemo(() => {
8    console.log('Obliczanie niebezpiecznych asteroid...');
9    return asteroids.filter(asteroid => {
10      // Symulacja kosztownych obliczeń
11      let isDangerous = false;
12      for (let i = 0; i < 10000; i++) {
13        if (asteroid.velocity > 20 && asteroid.size > 1000 && asteroid.distance < 50000000) {
14          isDangerous = true;
15        }
16      }
17      return isDangerous;
18    });
19  }, [asteroids]); // Obliczenie zostanie wykonane ponownie tylko gdy zmieni się tablica asteroids
20
21  // To obliczenie również jest memoizowane, ale zależy od filtru
22  const displayedAsteroids = useMemo(() => {
23    console.log('Filtrowanie asteroid...');
24    if (filter === 'all') return asteroids;
25    if (filter === 'dangerous') return dangerousAsteroids;
26    return [];
27  }, [filter, asteroids, dangerousAsteroids]);
28
29  return (
30    <div>
31      <div>
32        <button onClick={() => setFilter('all')}>Wszystkie</button>
33        <button onClick={() => setFilter('dangerous')}>Niebezpieczne</button>
34      </div>
35      <h2>Asteroidy ({displayedAsteroids.length}):</h2>
36      <ul>
37        {displayedAsteroids.map(asteroid => (
38          <li key={asteroid.id}>{asteroid.name}</li>
39        ))}
40      </ul>
41    </div>
42  );
43}

useCallback - memoizacja funkcji

Hook useCallback jest podobny do useMemo, ale zamiast zapamiętywać wynik funkcji, zapamiętuje samą funkcję. Jest szczególnie przydatny, gdy przekazujesz funkcje jako propsy do memoizowanych komponentów potomnych.

1import React, { useState, useCallback } from 'react';
2
3function MissionControl() {
4  const [missionStatus, setMissionStatus] = useState('preparing');
5  const [fuelLevel, setFuelLevel] = useState(100);
6
7  // Z useCallback, funkcja jest tworzona ponownie tylko wtedy, gdy zmienia się fuelLevel
8  const startMission = useCallback(() => {
9    if (fuelLevel > 20) {
10      setMissionStatus('in-progress');
11      console.log(`Misja rozpoczęta z poziomem paliwa: ${fuelLevel}%`);
12    } else {
13      console.log('Za mało paliwa do rozpoczęcia misji');
14    }
15  }, [fuelLevel]); // Zależność: funkcja utworzona ponownie, gdy zmieni się fuelLevel
16
17  const abortMission = useCallback(() => {
18    setMissionStatus('aborted');
19    console.log('Misja przerwana!');
20  }, []); // Pusta tablica zależności - funkcja utworzona tylko raz
21
22  return (
23    <div>
24      <h2>Status misji: {missionStatus}</h2>
25      <p>Poziom paliwa: {fuelLevel}%</p>
26
27      <button onClick={() => setFuelLevel(prev => Math.max(0, prev - 10))}>
28        Zużyj paliwo
29      </button>
30
31      {/* Przekazanie memoizowanych funkcji jako propsów */}
32      <MemoizedControlPanel
33        onStart={startMission}
34        onAbort={abortMission}
35        fuelLevel={fuelLevel}
36      />
37    </div>
38  );
39}
40
41// Memoizowany komponent potomny, który otrzymuje funkcje jako propsy
42const ControlPanel = React.memo(({ onStart, onAbort, fuelLevel }) => {
43  console.log('Renderowanie panelu kontrolnego');
44
45  return (
46    <div className="control-panel">
47      <h3>Panel sterowania</h3>
48      <button onClick={onStart} disabled={fuelLevel <= 20}>
49        Start
50      </button>
51      <button onClick={onAbort}>
52        Przerwij misję
53      </button>
54    </div>
55  );
56});
57
58const MemoizedControlPanel = React.memo(ControlPanel);

Porównanie technik memoizacji i kiedy ich używać

TechnikaZastosowanieKiedy używać
React.memoKomponentGdy komponent często otrzymuje te same propsy
useMemoWartośćPrzy kosztownych obliczeniach i złożonych strukturach danych
useCallbackFunkcjaGdy przekazujesz funkcje jako propsy do memoizowanych komponentów

Typowe błędy i pułapki

1. Nadmierna memoizacja

1// Nieoptymalne - zbyt prosta wartość do memoizacji
2const count = useMemo(() => 1 + 2, []);
3
4// Nieoptymalne - prosta funkcja, która nie potrzebuje memoizacji
5const handleClick = useCallback(() => {
6  console.log('Kliknięto!');
7}, []);

2. Zapominanie o zależnościach

1// Niebezpieczne - brakuje zależności 'value'
2const processedData = useMemo(() => {
3  return heavyProcessing(value);
4}, []); // Powinno być [value]

3. Zbyt wiele zależności

1// Może zmniejszyć skuteczność memoizacji
2const result = useMemo(() => {
3  return computeResult(a, b);
4}, [a, b, c, d, e]); // Zbyt dużo zależności - memoizacja może być nieskuteczna

Mądrze korzystaj z memoizacji

  1. Nie optymalizuj przedwcześnie - używaj najpierw profilerów, aby zidentyfikować rzeczywiste problemy wydajnościowe
  2. Stosuj memoizację selektywnie - tylko tam, gdzie jest faktycznie potrzebna
  3. Pamiętaj o koszcie - memoizacja również ma swój koszt, który może przewyższać zyski dla prostszych operacji

Podsumowanie

Memoizacja w React to potężna technika optymalizacyjna, która - podobnie jak oszczędzanie zasobów na statku kosmicznym - pomaga efektywnie zarządzać wydajnością aplikacji. Korzystaj z niej mądrze:

  • Używaj React.memo dla komponentów, które często renderują się ponownie z tymi samymi propsami
  • Stosuj useMemo dla kosztownych obliczeń i tworzenia złożonych struktur danych
  • Wykorzystuj useCallback do stabilizowania referencji funkcji przekazywanych w dół drzewa komponentów

Pamiętaj jednak, że jak każda technika optymalizacyjna, memoizacja ma swój koszt i powinna być stosowana rozważnie, tam gdzie faktycznie przynosi korzyści wydajnościowe.

Kod do tej lekcji: App.jsx
1import React, { useState, useMemo, useCallback, memo, useRef } from 'react';
2
3// React.memo - memoizacja komponentów
4const SpaceShip = memo(function SpaceShip({ name, speed, onSelect }) {
5  console.log(`Rendering SpaceShip: ${name}`);
6
7  return (
8    <div className="spaceship" onClick={() => onSelect(name)}>
9      <h3>{name}</h3>
10      <p>Prędkość: {speed} km/s</p>
11    </div>
12  );
13});
14
15// useMemo - memoizacja wartości
16function ExpensiveCalculation({ numbers }) {
17  const [filter, setFilter] = useState('');
18
19  // useMemo zapobiega ponownemu obliczaniu przy każdym renderze
20  const stats = useMemo(() => {
21    console.log('Obliczanie statystyk...');
22
23    // Symulacja kosztownej operacji
24    const sum = numbers.reduce((a, b) => a + b, 0);
25    const avg = sum / numbers.length;
26    const max = Math.max(...numbers);
27    const min = Math.min(...numbers);
28
29    return { sum, avg, max, min };
30  }, [numbers]); // Tylko gdy numbers się zmieni
31
32  const filteredNumbers = useMemo(() => {
33    if (!filter) return numbers;
34    const threshold = parseInt(filter) || 0;
35    return numbers.filter(n => n >= threshold);
36  }, [numbers, filter]);
37
38  return (
39    <div className="demo-card">
40      <h2>useMemo - Statystyki</h2>
41      <div className="stats">
42        <div className="stat">Suma: {stats.sum}</div>
43        <div className="stat">Średnia: {stats.avg.toFixed(2)}</div>
44        <div className="stat">Min: {stats.min}</div>
45        <div className="stat">Max: {stats.max}</div>
46      </div>
47      <input
48        type="number"
49        value={filter}
50        onChange={(e) => setFilter(e.target.value)}
51        placeholder="Filtruj >= wartość"
52      />
53      <div className="filtered">
54        Przefiltrowane: [{filteredNumbers.join(', ')}]
55      </div>
56    </div>
57  );
58}
59
60// useCallback - memoizacja funkcji
61function SpaceFleet() {
62  const [ships] = useState([
63    { name: 'Apollo', speed: 11 },
64    { name: 'Voyager', speed: 17 },
65    { name: 'Enterprise', speed: 299792 },
66  ]);
67  const [selected, setSelected] = useState(null);
68  const [count, setCount] = useState(0);
69
70  // Bez useCallback - nowa funkcja przy każdym renderze
71  // const handleSelect = (name) => setSelected(name);
72
73  // Z useCallback - ta sama referencja funkcji
74  const handleSelect = useCallback((name) => {
75    setSelected(name);
76  }, []);
77
78  return (
79    <div className="demo-card">
80      <h2>useCallback - Flota</h2>
81      <p className="hint">Otwórz konsolę - zobacz które statki się re-renderują</p>
82
83      <div className="fleet">
84        {ships.map(ship => (
85          <SpaceShip
86            key={ship.name}
87            name={ship.name}
88            speed={ship.speed}
89            onSelect={handleSelect}
90          />
91        ))}
92      </div>
93
94      {selected && <p className="selected">Wybrano: {selected}</p>}
95
96      <div className="counter-section">
97        <button onClick={() => setCount(c => c + 1)}>
98          Licznik: {count} (powoduje re-render)
99        </button>
100      </div>
101    </div>
102  );
103}
104
105// Porównanie z i bez memoizacji
106function MemoizationComparison() {
107  const [value, setValue] = useState(0);
108  const calls = useRef({ with: 0, without: 0 });
109
110  // Funkcja BEZ memoizacji
111  const expensiveWithout = () => {
112    calls.current.without += 1;
113    let result = 0;
114    for (let i = 0; i < 1000; i++) {
115      result += Math.sqrt(i);
116    }
117    return result;
118  };
119
120  // Funkcja Z memoizacją
121  const expensiveWith = useMemo(() => {
122    calls.current.with += 1;
123    let result = 0;
124    for (let i = 0; i < 1000; i++) {
125      result += Math.sqrt(i);
126    }
127    return result;
128  }, []); // Pusta tablica = oblicz raz
129
130  const resultWithout = expensiveWithout();
131
132  return (
133    <div className="demo-card">
134      <h2>Porównanie</h2>
135      <div className="comparison">
136        <div className="box without">
137          <h3>Bez useMemo</h3>
138          <p>Wywołań: {calls.current.without}</p>
139          <p className="result">{resultWithout.toFixed(2)}</p>
140        </div>
141        <div className="box with">
142          <h3>Z useMemo</h3>
143          <p>Wywołań: {calls.current.with}</p>
144          <p className="result">{expensiveWith.toFixed(2)}</p>
145        </div>
146      </div>
147      <button onClick={() => setValue(v => v + 1)}>
148        Wymuś re-render ({value})
149      </button>
150    </div>
151  );
152}
153
154function App() {
155  const [numbers] = useState([23, 45, 12, 67, 89, 34, 56, 78, 90, 11]);
156
157  return (
158    <div className="app">
159      <h1>Memoizacja w React</h1>
160      <p className="subtitle">React.memo, useMemo, useCallback</p>
161      <div className="demos">
162        <ExpensiveCalculation numbers={numbers} />
163        <SpaceFleet />
164        <MemoizationComparison />
165      </div>
166    </div>
167  );
168}
169
170export default App;

Widzisz błąd w tej lekcji?

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż poprawną składnię useMemo do memoizacji obliczeń:

Przydatne artykuły