Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Memoizacja - React.memo, useMemo i useCallback
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?
- Przy kosztownych obliczeniach
- Przy tworzeniu obiektów referencyjnych jako propsy dla komponentów potomnych
- 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ć
| Technika | Zastosowanie | Kiedy używać |
|---|---|---|
| React.memo | Komponent | Gdy komponent często otrzymuje te same propsy |
| useMemo | Wartość | Przy kosztownych obliczeniach i złożonych strukturach danych |
| useCallback | Funkcja | Gdy 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ć nieskutecznaMądrze korzystaj z memoizacji
- Nie optymalizuj przedwcześnie - używaj najpierw profilerów, aby zidentyfikować rzeczywiste problemy wydajnościowe
- Stosuj memoizację selektywnie - tylko tam, gdzie jest faktycznie potrzebna
- 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.memodla komponentów, które często renderują się ponownie z tymi samymi propsami - Stosuj
useMemodla kosztownych obliczeń i tworzenia złożonych struktur danych - Wykorzystuj
useCallbackdo 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ń: