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.
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 :
setState ou dispatch déclenche un re-rendu.La deuxième règle est celle qui surprend la plupart des développeurs. Considère cet exemple :
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ûteuxuseCallback — mettre en cache une référence de fonction pour qu'elle ne change pas à chaque renduReact.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).
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.
React.memo utilise une comparaison superficielle par défaut. Cela signifie :
'Alice' === 'Alice' est vrai.{} === {} 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 :
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.
Tu peux passer une fonction de comparaison personnalisée comme deuxième argument à React.memo :
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 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.
const memoizedValue = React.useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);1. Calculs coûteux :
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 :
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.
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 est essentiellement useMemo pour les fonctions. Il retourne une version mémorisée du callback qui change uniquement quand ses dépendances changent.
const memoizedFn = React.useCallback(() => {
doSomething(a, b);
}, [a, b]);Ceci est équivalent à :
const memoizedFn = React.useMemo(() => {
return () => doSomething(a, b);
}, [a, b]);En JavaScript, chaque expression de fonction crée un nouvel objet fonction :
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.
Utilise useCallback quand :
React.memo)useEffect, useMemo, ou d'un autre hookN'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.
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.
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 :
Avant de recourir aux outils d'optimisation, demande-toi :
<p> n'a pas besoin de React.memo.React DevTools inclut un onglet Profiler qui enregistre les rendus :
Cherche les composants qui :
React.memo)Profile d'abord, puis optimise les goulots d'étranglement. Ne devine jamais où se trouve le problème de performance.
<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 ?