Optimisation du rendu avec React.memo et useCallback dans React
Dans une application React, un composant est rendu dans deux situations principales :
- Lorsque son état change (via un
setState
). - Lorsque son composant parent est rendu, c'est-à-dire réexécuté, même si ce composant enfant n'a pas subi de modification directe.
Dans certains cas, cela ne pose pas de problème, car la logique du composant enfant est simple ou rapide à exécuter. Toutefois, dans d'autres situations, le fait de rendre systématiquement un composant enfant, uniquement parce que son parent a été rendu, peut entraîner des problèmes de performances, surtout si le composant enfant contient des calculs complexes ou des rendus coûteux qui n'ont pas besoin d'être réexécutés.
Prenons un exemple :
Imaginons une application avec un composant parent ParentComponent
qui contient un composant enfant ChildComponent
. Le composant parent gère un état qui se met à jour fréquemment, tandis que le composant enfant effectue des calculs complexes dans son rendu.
import {useState} from "react";
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Incrémenter</button>
<p>{count}</p>
<ChildComponent />
</div>
);
}
function ChildComponent() {
return <div>Je suis le composant enfant !</div>;
}
export default function App() {
return (
<div>
<ParentComponent />
</div>
)
}
Dans cet exemple, chaque fois que l'état de ParentComponent
change (lorsque setCount
est appelé), le composant ChildComponent
est également rendu, même si l'état de ChildComponent
n'a pas changé. Cela peut entraîner des rendus inutiles et des problèmes de performance, surtout si la logique du composant enfant est coûteuse.
Pour remédier à cela, il existe deux solutions principales :
Meilleure structuration des composants React
Une façon de résoudre ce problème est de repenser la structure des composants pour éviter des rendus inutiles. Par exemple, vous pouvez extraire ChildComponent
du rendu de ParentComponent
, de manière à ce qu'il ne soit pas rendu à chaque fois que l'état du parent change.
import {useState} from "react";
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Incrémenter</button>
<p>{count}</p>
</div>
);
}
function ChildComponent() {
return <div>Je suis le composant enfant !</div>;
}
export default function App() {
return (
<div>
<ParentComponent />
<ChildComponent />
</div>
)
}
Utilisation de React.memo
Parfois, il n'est pas possible de restructurer les composants. C'est là que React.memo
intervient. React.memo
est une fonction qui mémorise le rendu d'un composant.
Si les propriétés du composant n'ont pas changé entre les rendus, React.memo
empêche le composant de se rendre à nouveau.
import {memo, useState} from "react";
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Incrémenter</button>
<p>{count}</p>
<MemorizedChildComponent />
</div>
);
}
const MemorizedChildComponent = memo(function ChildComponent() {
return <div>Je suis le composant enfant mémorisé !</div>;
})
export default function App() {
return (
<div>
<ParentComponent />
</div>
)
}
Attention à l'utilisation excessive de React.memo
Il est important de ne pas abuser de l'utilisation de React.memo
si le composant concerné change fréquemment d'état. En effet, cela pourrait entraîner une perte de performance plutôt qu'un gain, car la mise en cache des résultats pourrait devenir contre-productive. En effet, si un composant met fréquemment à jour son état, React.memo
pourrait engendrer des coûts supplémentaires liés à la gestion de la mémoire et au suivi des comparaisons, rendant l'optimisation moins efficace.
Cela est souvent évident lorsqu'on passe des paramètres de type primitif, mais il est moins évident lorsqu'il s'agit d'objets, de tableaux ou de gestionnaires d'événements. Même si la valeur de ces paramètres ne change pas, si le composant parent est ré-rendu, React considérera qu'il s'agit de nouveaux paramètres en raison du changement de référence.
Voici un exemple pour mieux comprendre :
import {memo, useState} from "react";
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = () => console.log('clicked !');
return (
<div>
<button onClick={() => setCount(count + 1)}>Incrémenter</button>
<p>{count}</p>
<MemorizedChildComponent handleClick={handleClick} />
</div>
);
}
const MemorizedChildComponent = memo(function ChildComponent({handleClick}) {
return <div onClick={handleClick}>Je suis le composant enfant mémorisé !</div>;
})
export default function App() {
return (
<div>
<ParentComponent />
</div>
)
}
Utilisation de useCallback
Pour résoudre ce problème, vous pouvez utiliser useMemo
afin de mémoriser le gestionnaire d'événements et éviter qu'une nouvelle référence ne soit générée à chaque rendu du composant parent :
const handleClick = useMemo(() => {
return () => console.log('clicked !')
}, []);
Une approche plus simple consiste à utiliser le hook useCallback
, qui représente un raccourci pour la syntaxe mentionnée précédemment :
const handleClick = useCallback(() => console.log('clicked !'), []);
Cela permet de conserver la même référence pour handleClick
et d'éviter les rendus inutiles du composant enfant.