Cours JavaScript et React · Module 5: useEffect et hooks personnalisés
Mémoïsation - React.memo, useMemo et useCallback
Dans cette leçon8
Dans l'espace, les ressources sont limitées et doivent être utilisées avec sagesse. De même dans le monde de React, les performances de l'application sont d'une importance capitale. La mémoïsation est une technique d'optimisation qui permet de « mémoriser » les résultats d'opérations coûteuses et de les réutiliser lorsque les données d'entrée n'ont pas changé - un peu comme sauvegarder et réutiliser des calculs de trajectoire au lieu de les refaire de zéro.
Qu'est-ce que la mémoïsation en React ?
La mémoïsation en React est un mécanisme d'optimisation des performances qui te permet de :
- Éviter les re-rendus inutiles des composants
- Empêcher la ré-exécution de calculs coûteux
- Stabiliser les références vers les fonctions et les objets
React.memo - mémoïsation de composant
React.memo est un composant d'ordre supérieur (HOC) qui enveloppe un composant et l'empêche de se re-rendre si ses props n'ont pas changé.
Utilisation de base de React.memo
1import React from 'react';
2
3// Composant sans mémoïsation
4function PlanetInfo({ name, diameter, distance }) {
5 console.log(`Rendu des infos de la planète : ${name}`);
6 return (
7 <div className="planet-info">
8 <h2>{name}</h2>
9 <p>Diamètre : {diameter} km</p>
10 <p>Distance au Soleil : {distance} millions de km</p>
11 </div>
12 );
13}
14
15// Même composant avec mémoïsation
16const MemoizedPlanetInfo = React.memo(PlanetInfo);
17
18// Composant parent
19function SolarSystem() {
20 const [count, setCount] = useState(0);
21
22 return (
23 <div>
24 <button onClick={() => setCount(count + 1)}>
25 Compteur : {count}
26 </button>
27
28 {/* Ce composant ne se re-rendra que lorsque ses props changeront */}
29 <MemoizedPlanetInfo
30 name="Mars"
31 diameter={6779}
32 distance={228}
33 />
34 </div>
35 );
36}Remarque que dans l'exemple ci-dessus, même si le composant parent SolarSystem se re-rend à chaque clic sur le bouton, le composant MemoizedPlanetInfo ne se re-rendra pas car ses props restent inchangées.
Comparaison personnalisée des props
Par défaut, React.memo compare les props avec une comparaison superficielle. Cependant, tu peux fournir ta propre fonction de comparaison :
1const MemoizedPlanetInfo = React.memo(
2 PlanetInfo,
3 (prevProps, nextProps) => {
4 // Retourne true si tu veux empêcher le re-rendu
5 // Retourne false si tu veux re-rendre le composant
6 return prevProps.name === nextProps.name &&
7 prevProps.diameter === nextProps.diameter;
8 // Note que nous ignorons les changements de distance - le composant ne se re-rendra pas
9 // lorsque seule cette propriété change
10 }
11);useMemo - mémoïsation de valeur
Le hook useMemo permet de mémoriser le résultat d'une opération coûteuse et de le réutiliser tant que les dépendances n'ont pas changé.
Quand utiliser useMemo ?
- Pour les calculs coûteux
- Lors de la création d'objets de référence à passer en props à des composants enfants
- Lors de la création de grandes structures de données
1import React, { useState, useMemo } from 'react';
2
3function AsteroidCalculator({ asteroids }) {
4 const [filter, setFilter] = useState('all');
5
6 // Calcul coûteux - mémoïsé pour de meilleures performances
7 const dangerousAsteroids = useMemo(() => {
8 console.log('Calcul des astéroïdes dangereux...');
9 return asteroids.filter(asteroid => {
10 // Simulation de calculs coûteux
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]); // Le calcul ne sera ré-exécuté que lorsque le tableau asteroids change
20
21 // Ce calcul est aussi mémoïsé mais dépend du filtre
22 const displayedAsteroids = useMemo(() => {
23 console.log('Filtrage des astéroïdes...');
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')}>Tous</button>
33 <button onClick={() => setFilter('dangerous')}>Dangereux</button>
34 </div>
35 <h2>Astéroïdes ({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 - mémoïsation de fonction
Le hook useCallback est similaire à useMemo, mais au lieu de mémoriser le résultat de la fonction, il mémorise la fonction elle-même. Il est particulièrement utile lorsqu'on passe des fonctions en props à des composants enfants mémoïsés.
1import React, { useState, useCallback } from 'react';
2
3function MissionControl() {
4 const [missionStatus, setMissionStatus] = useState('preparing');
5 const [fuelLevel, setFuelLevel] = useState(100);
6
7 // Avec useCallback, la fonction n'est recréée que lorsque fuelLevel change
8 const startMission = useCallback(() => {
9 if (fuelLevel > 20) {
10 setMissionStatus('in-progress');
11 console.log(`Mission démarrée avec un niveau de carburant : ${fuelLevel}%`);
12 } else {
13 console.log('Pas assez de carburant pour démarrer la mission');
14 }
15 }, [fuelLevel]); // Dépendance : la fonction est recréée lorsque fuelLevel change
16
17 const abortMission = useCallback(() => {
18 setMissionStatus('aborted');
19 console.log('Mission interrompue !');
20 }, []); // Tableau de dépendances vide - la fonction n'est créée qu'une seule fois
21
22 return (
23 <div>
24 <h2>Statut de la mission : {missionStatus}</h2>
25 <p>Niveau de carburant : {fuelLevel}%</p>
26
27 <button onClick={() => setFuelLevel(prev => Math.max(0, prev - 10))}>
28 Utiliser du carburant
29 </button>
30
31 {/* Passer les fonctions mémoïsées en props */}
32 <MemoizedControlPanel
33 onStart={startMission}
34 onAbort={abortMission}
35 fuelLevel={fuelLevel}
36 />
37 </div>
38 );
39}
40
41// Composant enfant mémoïsé qui reçoit des fonctions en props
42const ControlPanel = React.memo(({ onStart, onAbort, fuelLevel }) => {
43 console.log('Rendu du panneau de contrôle');
44
45 return (
46 <div className="control-panel">
47 <h3>Panneau de contrôle</h3>
48 <button onClick={onStart} disabled={fuelLevel <= 20}>
49 Démarrer
50 </button>
51 <button onClick={onAbort}>
52 Interrompre la mission
53 </button>
54 </div>
55 );
56});
57
58const MemoizedControlPanel = React.memo(ControlPanel);Comparer les techniques de mémoïsation et quand les utiliser
| Technique | Cas d'usage | Quand l'utiliser |
|---|---|---|
| React.memo | Composant | Lorsqu'un composant reçoit fréquemment les mêmes props |
| useMemo | Valeur | Pour les calculs coûteux et les structures de données complexes |
| useCallback | Fonction | Lorsqu'on passe des fonctions en props à des composants mémoïsés |
Erreurs et pièges courants
1. Sur-mémoïsation
1// Pas optimal - valeur trop simple pour être mémoïsée
2const count = useMemo(() => 1 + 2, []);
3
4// Pas optimal - fonction simple qui n'a pas besoin de mémoïsation
5const handleClick = useCallback(() => {
6 console.log('Cliqué !');
7}, []);2. Oublier des dépendances
1// Pas sûr - dépendance 'value' manquante
2const processedData = useMemo(() => {
3 return heavyProcessing(value);
4}, []); // Devrait être [value]3. Trop de dépendances
1// Peut réduire l'efficacité de la mémoïsation
2const result = useMemo(() => {
3 return computeResult(a, b);
4}, [a, b, c, d, e]); // Trop de dépendances - la mémoïsation peut être inefficaceUtilise la mémoïsation avec sagesse
- N'optimise pas prématurément - utilise d'abord des profileurs pour identifier les véritables problèmes de performance
- Applique la mémoïsation de façon sélective - uniquement là où c'est réellement nécessaire
- Souviens-toi du coût - la mémoïsation a aussi son coût, qui peut dépasser les gains pour les opérations plus simples
Résumé
La mémoïsation en React est une technique d'optimisation puissante qui - comme la conservation des ressources sur un vaisseau spatial - aide à gérer efficacement les performances de l'application. Utilise-la avec sagesse :
- Utilise
React.memopour les composants qui se re-rendent fréquemment avec les mêmes props - Utilise
useMemopour les calculs coûteux et la création de structures de données complexes - Utilise
useCallbackpour stabiliser les références de fonctions passées dans l'arbre des composants
Souviens-toi cependant que, comme toute technique d'optimisation, la mémoïsation a son coût et doit être appliquée judicieusement, là où elle apporte réellement des bénéfices de performance.
Code de cette leçon : App.jsx
1import React, { useState, useMemo, useCallback, memo, useRef } from 'react';
2
3// React.memo - mémoïsation des composants
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>Vitesse : {speed} km/s</p>
11 </div>
12 );
13});
14
15// useMemo - mémoïsation de valeur
16function ExpensiveCalculation({ numbers }) {
17 const [filter, setFilter] = useState('');
18
19 // useMemo empêche le recalcul à chaque rendu
20 const stats = useMemo(() => {
21 console.log('Calcul des statistiques...');
22
23 // Simulation d'une opération coûteuse
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]); // Seulement quand numbers change
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 - Statistiques</h2>
41 <div className="stats">
42 <div className="stat">Somme : {stats.sum}</div>
43 <div className="stat">Moyenne : {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="Filtrer >= valeur"
52 />
53 <div className="filtered">
54 Filtrés : [{filteredNumbers.join(', ')}]
55 </div>
56 </div>
57 );
58}
59
60// useCallback - mémoïsation de fonction
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 // Sans useCallback - nouvelle fonction à chaque rendu
71 // const handleSelect = (name) => setSelected(name);
72
73 // Avec useCallback - même référence de fonction
74 const handleSelect = useCallback((name) => {
75 setSelected(name);
76 }, []);
77
78 return (
79 <div className="demo-card">
80 <h2>useCallback - Flotte</h2>
81 <p className="hint">Ouvre la console - vois quels vaisseaux se re-rendent</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">Sélectionné : {selected}</p>}
95
96 <div className="counter-section">
97 <button onClick={() => setCount(c => c + 1)}>
98 Counter: {count} (provoque un re-render)
99 </button>
100 </div>
101 </div>
102 );
103}
104
105// Comparaison avec et sans mémoïsation
106function MemoizationComparison() {
107 const [value, setValue] = useState(0);
108 const calls = useRef({ with: 0, without: 0 });
109
110 // Fonction SANS mémoïsation
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 // Fonction AVEC mémoïsation
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 }, []); // Tableau vide = calcule une seule fois
129
130 const resultWithout = expensiveWithout();
131
132 return (
133 <div className="demo-card">
134 <h2>Comparaison</h2>
135 <div className="comparison">
136 <div className="box without">
137 <h3>Sans useMemo</h3>
138 <p>Appels : {calls.current.without}</p>
139 <p className="result">{resultWithout.toFixed(2)}</p>
140 </div>
141 <div className="box with">
142 <h3>Avec useMemo</h3>
143 <p>Appels : {calls.current.with}</p>
144 <p className="result">{expensiveWith.toFixed(2)}</p>
145 </div>
146 </div>
147 <button onClick={() => setValue(v => v + 1)}>
148 Forcer le 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>Mémoïsation en 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;Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne la syntaxe correcte de useMemo pour mémoïser des calculs :