Passer au contenu principal

Optimisation du rendu avec React.memo et useCallback dans React

Dans une application React, un composant est rendu dans deux situations principales :

  1. Lorsque son état change (via un setState).
  2. 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.