Cours JavaScript et React · Module 5: useEffect et hooks personnalisés

Mémoïsation - React.memo, useMemo et useCallback

6 min de lecture
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 ?

  1. Pour les calculs coûteux
  2. Lors de la création d'objets de référence à passer en props à des composants enfants
  3. 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

TechniqueCas d'usageQuand l'utiliser
React.memoComposantLorsqu'un composant reçoit fréquemment les mêmes props
useMemoValeurPour les calculs coûteux et les structures de données complexes
useCallbackFonctionLorsqu'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 inefficace

Utilise la mémoïsation avec sagesse

  1. N'optimise pas prématurément - utilise d'abord des profileurs pour identifier les véritables problèmes de performance
  2. Applique la mémoïsation de façon sélective - uniquement là où c'est réellement nécessaire
  3. 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.memo pour les composants qui se re-rendent fréquemment avec les mêmes props
  • Utilise useMemo pour les calculs coûteux et la création de structures de données complexes
  • Utilise useCallback pour 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 :

Articles utiles