Avancé25 min de lecture

Performance : memo, useMemo, useCallback

Apprends les outils d'optimisation des performances de React : React.memo pour empêcher les re-rendus inutiles de composants, useMemo pour mettre en cache des calculs coûteux, et useCallback pour des références de fonctions stables.

Quand React effectue-t-il un re-rendu ?

Avant d'apprendre les outils d'optimisation, tu dois comprendre quand et pourquoi React effectue un re-rendu des composants.

Un composant effectue un re-rendu lorsque :

  1. Son state change — appeler setState ou dispatch déclenche un re-rendu.
  2. Son parent effectue un re-rendu — quand un composant parent effectue un re-rendu, tous ses enfants effectuent aussi un re-rendu, même si leurs props n'ont pas changé.
  3. La valeur de son contexte change — quand la valeur d'un Provider de contexte change, chaque consommateur effectue un re-rendu.

La deuxième règle est celle qui surprend la plupart des développeurs. Considère cet exemple :

jsx
function Parent() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <ExpensiveChild name="Alice" />  {/* re-rendu à chaque changement de count ! */}
    </div>
  );
}

function ExpensiveChild({ name }) {
  // Imagine que cela fait des calculs lourds ou rend une longue liste
  console.log('ExpensiveChild rendered');
  return <p>Hello, {name}!</p>;
}

À chaque fois que le count du parent change, ExpensiveChild effectue un re-rendu même si sa prop name ("Alice") ne change jamais. Pour des composants simples, ce n'est pas grave — le diffing de React est rapide. Mais pour des composants qui sont vraiment coûteux (longues listes, calculs complexes, arbres DOM lourds), ces re-rendus inutiles peuvent causer des ralentissements notables.

React fournit trois outils pour optimiser cela :

  • React.memo — éviter le re-rendu d'un composant quand ses props n'ont pas changé
  • useMemo — mettre en cache le résultat d'un calcul coûteux
  • useCallback — mettre en cache une référence de fonction pour qu'elle ne change pas à chaque rendu

React.memo : Mémoriser les composants

React.memo est un composant d'ordre supérieur qui enveloppe ton composant et évite le re-rendu si ses props n'ont pas changé (en utilisant une comparaison superficielle).

jsx
const ExpensiveChild = React.memo(function ExpensiveChild({ name }) {
  console.log('ExpensiveChild rendered');
  return <p>Hello, {name}!</p>;
});

Maintenant ExpensiveChild effectuera un re-rendu uniquement quand name change réellement. Quand le parent effectue un re-rendu à cause d'une mise à jour de count, React compare les anciennes props ({ name: 'Alice' }) avec les nouvelles props ({ name: 'Alice' }). Puisqu'elles sont identiques (égalité superficielle), React évite complètement le rendu de ExpensiveChild.

Comparaison superficielle

React.memo utilise une comparaison superficielle par défaut. Cela signifie :

  • Les primitives (chaînes de caractères, nombres, booléens) sont comparées par valeur : 'Alice' === 'Alice' est vrai.
  • Les objets, tableaux et fonctions sont comparés par référence : {} === {} est faux car ce sont des objets différents en mémoire.

C'est important ! Si tu passes un objet ou une fonction comme prop, une nouvelle référence est créée à chaque rendu :

jsx
function Parent() {
  const [count, setCount] = React.useState(0);

  // NOUVEL objet à chaque rendu — annule l'effet de React.memo !
  const style = { color: 'red' };

  // NOUVELLE fonction à chaque rendu — annule aussi l'effet de React.memo !
  const handleClick = () => console.log('clicked');

  return <MemoChild style={style} onClick={handleClick} />;
}

Même si les valeurs sont logiquement identiques, ce sont de nouvelles références à chaque fois, donc React.memo les voit comme "changées" et effectue quand même un re-rendu. C'est là que useMemo et useCallback entrent en jeu.

Fonction de comparaison personnalisée

Tu peux passer une fonction de comparaison personnalisée comme deuxième argument à React.memo :

jsx
const MemoChild = React.memo(MyComponent, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id; // re-rendu uniquement si id change
});

Retourne true pour éviter le re-rendu, false pour l'autoriser.

useMemo : Mettre en cache des calculs coûteux

useMemo prend une fonction et un tableau de dépendances, et retourne le résultat mémorisé. React ré-exécute la fonction uniquement quand l'une des dépendances change.

jsx
const memoizedValue = React.useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

Quand utiliser useMemo

1. Calculs coûteux :

jsx
function FilteredList({ items, filter }) {
  // Sans useMemo : s'exécute à CHAQUE rendu
  // Avec useMemo : s'exécute uniquement quand items ou filter changent
  const filtered = React.useMemo(() => {
    console.log('Filtering...');
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return (
    <ul>
      {filtered.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

2. Stabiliser les références d'objets/tableaux pour les enfants React.memo :

jsx
function Parent() {
  const [count, setCount] = React.useState(0);

  // Sans useMemo : nouveau tableau à chaque rendu
  // Avec useMemo : même référence sauf si les données changent
  const items = React.useMemo(() => ['Apple', 'Banana', 'Cherry'], []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <MemoizedList items={items} />
    </div>
  );
}

Parce que items a une référence stable (même objet tableau), l'enfant mémorisé évitera correctement le re-rendu quand count change.

Comment fonctionne useMemo

Au premier rendu, React appelle la fonction et stocke à la fois le résultat et les dépendances. Aux rendus suivants, React compare les dépendances actuelles avec celles stockées. Si elles sont identiques (comparaison superficielle), React retourne le résultat stocké sans rappeler la fonction. Si elles diffèrent, React appelle la fonction et stocke le nouveau résultat.

useCallback : Références de fonctions stables

useCallback est essentiellement useMemo pour les fonctions. Il retourne une version mémorisée du callback qui change uniquement quand ses dépendances changent.

jsx
const memoizedFn = React.useCallback(() => {
  doSomething(a, b);
}, [a, b]);

Ceci est équivalent à :

jsx
const memoizedFn = React.useMemo(() => {
  return () => doSomething(a, b);
}, [a, b]);

Pourquoi useCallback est important

En JavaScript, chaque expression de fonction crée un nouvel objet fonction :

jsx
function Parent() {
  const [count, setCount] = React.useState(0);

  // Sans useCallback : nouvelle fonction à chaque rendu
  const handleClick = () => console.log('clicked');

  // Avec useCallback : même référence de fonction à travers les rendus
  const handleClickStable = React.useCallback(() => {
    console.log('clicked');
  }, []);

  return <MemoChild onClick={handleClickStable} />;
}

Sans useCallback, handleClick est une nouvelle fonction à chaque rendu, ce qui signifie que React.memo sur MemoChild serait inutile — il verrait toujours une "nouvelle" prop onClick.

Avec useCallback, handleClickStable est la même référence de fonction à travers les rendus (tant que ses dépendances ne changent pas), donc React.memo peut correctement éviter le re-rendu.

Quand utiliser useCallback

Utilise useCallback quand :

  • Tu passes une fonction à un composant enfant mémorisé (React.memo)
  • Tu passes une fonction comme dépendance de useEffect, useMemo, ou d'un autre hook
  • La fonction est utilisée comme référence stable dans des abonnements d'événements

N'utilise PAS useCallback partout. Cela ajoute de la complexité et une surcharge mémoire. Utilise-le uniquement quand il y a un bénéfice mesurable.

Quand NE PAS optimiser

Le concept d'optimisation des performances le plus important est de savoir quand NE PAS optimiser. L'optimisation prématurée est un piège courant dans le développement React.

N'optimise pas par défaut

Le rendu de React est très rapide. Pour la plupart des composants, le coût d'un re-rendu est négligeable (microsecondes). Ajouter React.memo, useMemo, et useCallback partout a ses propres coûts :

  • Surcharge mémoire — les valeurs mémorisées sont stockées en mémoire
  • Coût de comparaison — React doit comparer les props/dépendances à chaque rendu
  • Complexité du code — plus de hooks signifie un code plus difficile à lire
  • Valeurs obsolètes — des tableaux de dépendances incorrects peuvent causer des bugs difficiles à traquer

La checklist d'optimisation

Avant de recourir aux outils d'optimisation, demande-toi :

  1. Y a-t-il réellement un problème de performance ? Si l'application semble rapide, n'optimise pas.
  2. Peux-tu mesurer l'amélioration ? Utilise le Profiler de React DevTools pour voir combien de temps prennent les rendus.
  3. Le composant est-il vraiment coûteux ? Un composant qui rend 3 balises <p> n'a pas besoin de React.memo.
  4. As-tu essayé des solutions plus simples d'abord ? Souvent, déplacer le state plus près de l'endroit où il est nécessaire (remonter ou colocaliser le state) élimine les re-rendus inutiles sans aucune mémorisation.

Bases du profilage

React DevTools inclut un onglet Profiler qui enregistre les rendus :

  1. Ouvre React DevTools et clique sur l'onglet Profiler
  2. Clique sur "Record" et interagis avec ton application
  3. Clique sur "Stop" pour voir un graphique en flammes des rendus
  4. Chaque barre montre un composant, son temps de rendu, et s'il a effectué un re-rendu

Cherche les composants qui :

  • Effectuent fréquemment un re-rendu (beaucoup de barres)
  • Prennent longtemps par rendu (barres larges)
  • Effectuent un re-rendu quand ils ne devraient pas (les barres grises deviennent vertes quand tu ajoutes React.memo)

Profile d'abord, puis optimise les goulots d'étranglement. Ne devine jamais où se trouve le problème de performance.

Optimisation des performances en action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  // Enfant mémorisé — effectue un re-rendu uniquement quand items change
  const ExpensiveList = React.memo(function ExpensiveList({ items }) {
    console.log("ExpensiveList rendered");
    return (
      <ul id="list">
        {items.map((item, i) => <li key={i}>{item}</li>)}
      </ul>
    );
  });

  function App() {
    const [count, setCount] = React.useState(0);

    // useMemo : référence de tableau stable
    const items = React.useMemo(() => ["React", "Vue", "Angular"], []);

    // useCallback : référence de fonction stable
    const increment = React.useCallback(() => {
      setCount(c => c + 1);
    }, []);

    return (
      <div>
        <h2>Démo de performance</h2>
        <p>Count: {count}</p>
        <button onClick={increment}>Incrémenter</button>
        <p id="render-info">Items: {items.length}</p>
        <ExpensiveList items={items} />
        <p><em>Ouvre la console — ExpensiveList n'effectue pas de re-rendu lors du changement de count !</em></p>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

Quel est le principal risque d'utiliser React.memo, useMemo et useCallback partout ?

Prêt à pratiquer ?

Crée ton compte gratuit pour accéder à l'éditeur de code interactif, lancer les défis et suivre ta progression.