React memo, useMemo, useCallback : performance

Front-end 24/03/2026 12:00:00 angularforall.com
React Memo Usememo Usecallback Performance
React memo, useMemo, useCallback : performance

Optimisez vos composants React avec memo, useMemo et useCallback. Évitez les re-rendus inutiles et boostez les performances de vos apps React.

Comprendre les re-rendus React

Avant d'optimiser quoi que ce soit, il faut comprendre pourquoi et quand React re-rend un composant. Un re-rendu se produit dans trois situations :

  • L'état local du composant change (setState)
  • Une prop reçue par le composant change
  • Le composant parent se re-rend (même si les props n'ont pas changé !)

Ce troisième point est souvent surprenant pour les débutants. Par défaut, quand un parent se re-rend, tous ses enfants se re-rendent aussi, même si leurs props sont identiques.

function Parent() {
    const [compteur, setCompteur] = useState(0);

    return (
        <div>
            <p>Compteur parent : {compteur}</p>
            <button onClick={() => setCompteur(c => c + 1)}>+1</button>

            {/* Enfant re-rendu à chaque clic, même si title ne change JAMAIS */}
            <Enfant title="Titre fixe" />
        </div>
    );
}

function Enfant({ title }) {
    // Cette fonction s'exécute à chaque re-rendu du Parent
    console.log('Enfant rendu');
    return <h2>{title}</h2>;
}

Dans cet exemple, chaque clic sur le bouton provoque le re-rendu de Enfant, alors que title n'a pas changé. Sur une application avec 50 composants, cette cascade peut devenir un problème de performance mesurable.

À retenir : Un re-rendu ne signifie pas forcément que le DOM est mis à jour. React compare l'ancien et le nouveau Virtual DOM (reconciliation) avant de toucher au vrai DOM. Mais le calcul du Virtual DOM lui-même a un coût — c'est ce qu'on cherche à éviter avec memo, useMemo et useCallback.
Outil Ce qu'il mémorise Comparaison
React.memo Un composant entier Props précédentes vs nouvelles
useMemo Le résultat d'un calcul Dépendances précédentes vs nouvelles
useCallback Une référence de fonction Dépendances précédentes vs nouvelles

React.memo : mémoriser un composant

React.memo est un Higher-Order Component (HOC) qui enveloppe un composant fonctionnel et lui dit : "Ne te re-rends que si tes props ont changé." C'est l'équivalent de PureComponent pour les composants fonctionnels.

Utilisation de base

import { memo } from 'react';

// Sans memo : ce composant re-rendrait à chaque re-rendu du parent
function CarteArticle({ titre, auteur, date }) {
    console.log(`Rendu de : ${titre}`);
    return (
        <div className="card">
            <h3>{titre}</h3>
            <p>Par {auteur} — {date}</p>
        </div>
    );
}

// Avec memo : React compare les props avant de décider de re-rendre
const CarteArticleMemo = memo(function CarteArticle({ titre, auteur, date }) {
    console.log(`Rendu de : ${titre}`);
    return (
        <div className="card">
            <h3>{titre}</h3>
            <p>Par {auteur} — {date}</p>
        </div>
    );
});

// Syntaxe alternative avec export direct
export const CarteArticle = memo(({ titre, auteur, date }) => {
    return (
        <div className="card">
            <h3>{titre}</h3>
            <p>Par {auteur} — {date}</p>
        </div>
    );
});

React.memo effectue une comparaison superficielle (shallow equality) des props. Deux valeurs primitives (string, number, boolean) sont considérées égales si elles ont la même valeur. Deux objets ou tableaux sont considérés différents si leur référence a changé — même si leur contenu est identique.

Le problème des props objet et fonction

function Parent() {
    const [compteur, setCompteur] = useState(0);

    // ❌ PROBLÈME : ce tableau est recréé à chaque rendu du Parent
    // Sa référence change → memo ne sert à rien !
    const filtres = ['actif', 'recent'];

    // ❌ PROBLÈME : cette fonction est recréée à chaque rendu
    const handleClick = () => console.log('clic');

    return (
        <div>
            <button onClick={() => setCompteur(c => c + 1)}>+1</button>
            {/* CarteArticle avec memo va quand même se re-rendre ! */}
            <CarteArticleMemo
                titre="Mon article"
                filtres={filtres}    // Nouvelle référence à chaque rendu
                onClick={handleClick} // Nouvelle référence à chaque rendu
            />
        </div>
    );
}
C'est exactement pour résoudre ce problème que useMemo et useCallback existent. memo seul est rarement suffisant — il faut stabiliser les props objets et fonctions.

Comparateur personnalisé avec memo

// Pour une comparaison plus fine, passez un deuxième argument à memo
const CarteUtilisateur = memo(
    function CarteUtilisateur({ utilisateur, onSelect }) {
        return (
            <div onClick={() => onSelect(utilisateur.id)}>
                <p>{utilisateur.nom} — Score: {utilisateur.score}</p>
            </div>
        );
    },
    // Fonction de comparaison personnalisée
    // Retourne true = props identiques = pas de re-rendu
    (propsPrec, propsNouv) => {
        // Re-rendre seulement si l'id ou le score change
        // Ignorer les autres champs de l'objet utilisateur
        return (
            propsPrec.utilisateur.id === propsNouv.utilisateur.id &&
            propsPrec.utilisateur.score === propsNouv.utilisateur.score
        );
    }
);

useMemo : mémoriser un calcul coûteux

useMemo mémorise le résultat d'un calcul entre les rendus. React ne recalcule la valeur que lorsque les dépendances spécifiées changent. Entre-temps, il retourne la même valeur mémorisée.

Syntaxe et cas d'usage typique

import { useState, useMemo } from 'react';

function TableauDonnees({ donnees, critereRecherche, tri }) {
    // ❌ Sans useMemo : ce calcul s'exécute à CHAQUE rendu
    // const resultatsFiltres = filtrerEtTrier(donnees, critereRecherche, tri);

    // ✅ Avec useMemo : recalcul uniquement quand les dépendances changent
    const resultatsFiltres = useMemo(() => {
        console.log('Recalcul du filtrage...'); // Pour observer les rendus

        // Opération potentiellement lourde sur un grand tableau
        return donnees
            .filter(item =>
                item.nom.toLowerCase().includes(critereRecherche.toLowerCase())
            )
            .sort((a, b) => {
                if (tri === 'nom') return a.nom.localeCompare(b.nom);
                if (tri === 'date') return new Date(b.date) - new Date(a.date);
                return a.id - b.id;
            });
    }, [donnees, critereRecherche, tri]); // Recalculer si ces valeurs changent

    return (
        <ul>
            {resultatsFiltres.map(item => (
                <li key={item.id}>{item.nom} — {item.date}</li>
            ))}
        </ul>
    );
}

Stabiliser une référence d'objet avec useMemo

function ConfigProvider({ theme, langue }) {
    // ❌ Sans useMemo : config est un nouvel objet à chaque rendu
    // const config = { theme, langue, version: '2.0' };

    // ✅ Avec useMemo : même référence si theme et langue n'ont pas changé
    const config = useMemo(() => ({
        theme,
        langue,
        version: '2.0',
        // Calculs dérivés inclus dans la mémorisation
        estModeSombre: theme === 'dark',
        directionTexte: langue === 'ar' ? 'rtl' : 'ltr',
    }), [theme, langue]);

    // config garde la même référence → les enfants mémorisés ne re-rendent pas
    return <ComposantEnfant config={config} />;
}

useMemo pour des calculs statistiques

function DashboardVentes({ transactions }) {
    // Calcul de statistiques sur un tableau potentiellement grand
    const statistiques = useMemo(() => {
        if (!transactions.length) {
            return { total: 0, moyenne: 0, max: 0, min: 0 };
        }

        const montants = transactions.map(t => t.montant);
        const total    = montants.reduce((acc, val) => acc + val, 0);

        return {
            total,
            moyenne: Math.round(total / montants.length),
            max:     Math.max(...montants),
            min:     Math.min(...montants),
            nbTransactions: transactions.length,
        };
    }, [transactions]); // Recalculer uniquement quand transactions change

    return (
        <div className="row">
            <div className="col-3"><strong>Total :</strong> {statistiques.total}€</div>
            <div className="col-3"><strong>Moyenne :</strong> {statistiques.moyenne}€</div>
            <div className="col-3"><strong>Max :</strong> {statistiques.max}€</div>
            <div className="col-3"><strong>Min :</strong> {statistiques.min}€</div>
        </div>
    );
}
Règle des 3 conditions : N'utilisez useMemo que si (1) le calcul est réellement coûteux (traitement de tableau >1000 éléments, parsing JSON lourd…), (2) le composant re-rend souvent, et (3) vous avez mesuré un problème avec le Profiler. Ne mémorisez pas par précaution.

useCallback : mémoriser une fonction

useCallback retourne une version mémorisée d'une fonction. En JavaScript, chaque fois qu'un composant se re-rend, toutes les fonctions définies à l'intérieur sont recréées — elles ont une nouvelle référence. useCallback garde la même référence tant que les dépendances n'ont pas changé.

Pourquoi les références de fonctions posent problème

// En JavaScript, deux fonctions identiques ne sont JAMAIS égales par référence
const fn1 = () => console.log('hello');
const fn2 = () => console.log('hello');
console.log(fn1 === fn2); // false !

// C'est le même comportement dans React :
function Parent() {
    // À chaque rendu du Parent, handleDelete est une NOUVELLE fonction
    const handleDelete = (id) => {
        // logique de suppression
    };
    // → Si handleDelete est passée à un composant mémorisé,
    //   ce composant va se re-rendre quand même (nouvelle référence)
}

useCallback en pratique

import { useState, useCallback, memo } from 'react';

// Composant enfant mémorisé — ne doit se re-rendre qu'en cas de besoin
const BoutonAction = memo(function BoutonAction({ label, onClick }) {
    console.log(`BoutonAction "${label}" rendu`);
    return <button onClick={onClick}>{label}</button>;
});

function GestionnaireArticles() {
    const [articles, setArticles] = useState([
        { id: 1, titre: 'React Hooks' },
        { id: 2, titre: 'React Memo' },
    ]);
    const [selection, setSelection] = useState(null);

    // ❌ Sans useCallback : nouvelle référence à chaque rendu → BoutonAction re-rend
    // const handleSupprimer = (id) => setArticles(prev => prev.filter(a => a.id !== id));

    // ✅ Avec useCallback : même référence entre les rendus → BoutonAction stable
    const handleSupprimer = useCallback((id) => {
        // Forme fonctionnelle car on dépend de l'état précédent
        setArticles(prev => prev.filter(article => article.id !== id));
    }, []); // Pas de dépendances : setArticles est stable par garantie React

    const handleSelectionner = useCallback((id) => {
        setSelection(id);
    }, []); // Stable également

    return (
        <div>
            {articles.map(article => (
                <div key={article.id}>
                    <span>{article.titre}</span>
                    <BoutonAction
                        label="Sélectionner"
                        onClick={() => handleSelectionner(article.id)}
                    />
                    <BoutonAction
                        label="Supprimer"
                        onClick={() => handleSupprimer(article.id)}
                    />
                </div>
            ))}
            {selection && <p>Sélectionné : article #{selection}</p>}
        </div>
    );
}

useCallback avec des dépendances

function RechercheAvancee({ categorieActive, onResultat }) {
    const [terme, setTerme] = useState('');

    // Cette fonction dépend de terme ET categorieActive
    // Elle se recrée uniquement quand ces deux valeurs changent
    const rechercherAPI = useCallback(async () => {
        if (!terme.trim()) return; // Pas de recherche si vide

        const params = new URLSearchParams({
            q: terme,
            categorie: categorieActive,
        });

        const response = await fetch(`/api/recherche?${params}`);
        const resultats = await response.json();

        // Appeler le callback parent avec les résultats
        onResultat(resultats);
    }, [terme, categorieActive, onResultat]); // Dépendances déclarées

    return (
        <div>
            <input
                value={terme}
                onChange={(e) => setTerme(e.target.value)}
                placeholder="Rechercher..."
            />
            <button onClick={rechercherAPI}>Rechercher</button>
        </div>
    );
}
Astuce : Les fonctions de mise à jour de useState (setCompteur, setArticles…) sont garanties stables par React — leur référence ne change jamais. Vous n'avez pas besoin de les inclure dans les dépendances de useCallback.

memo + useCallback : le duo gagnant

La puissance réelle apparaît quand vous combinez memo sur le composant enfant et useCallback sur les fonctions passées en props. Les deux travaillent ensemble pour garantir que l'enfant ne se re-rend pas inutilement.

import { useState, useCallback, useMemo, memo } from 'react';

// --- Composant enfant mémorisé ---
const LigneTableau = memo(function LigneTableau({
    item,
    estSelectionne,
    onSelectionner,
    onSupprimer,
}) {
    console.log(`LigneTableau ${item.id} rendue`);
    return (
        <tr style={{ background: estSelectionne ? '#e3f2fd' : 'transparent' }}>
            <td>{item.nom}</td>
            <td>{item.categorie}</td>
            <td>{item.prix}€</td>
            <td>
                <button onClick={() => onSelectionner(item.id)}>
                    {estSelectionne ? '✓' : 'Sélectionner'}
                </button>
                <button onClick={() => onSupprimer(item.id)}>Supprimer</button>
            </td>
        </tr>
    );
});

// --- Composant parent orchestrateur ---
function TableauProduits({ produits }) {
    const [selection, setSelection] = useState(new Set());
    const [filtre, setFiltre]       = useState('');

    // useMemo : filtrage recalculé seulement quand produits ou filtre change
    const produitsFiltres = useMemo(() =>
        produits.filter(p =>
            p.nom.toLowerCase().includes(filtre.toLowerCase())
        ),
        [produits, filtre]
    );

    // useCallback : référence stable → LigneTableau ne re-rend pas pour ça
    const handleSelectionner = useCallback((id) => {
        setSelection(prev => {
            const nouvelle = new Set(prev);
            // Basculer la sélection : présent → retirer, absent → ajouter
            if (nouvelle.has(id)) {
                nouvelle.delete(id);
            } else {
                nouvelle.add(id);
            }
            return nouvelle;
        });
    }, []);

    const handleSupprimer = useCallback((id) => {
        // Note : la suppression réelle irait via une API ou un state global
        console.log(`Supprimer article ${id}`);
    }, []);

    return (
        <div>
            <input
                value={filtre}
                onChange={(e) => setFiltre(e.target.value)}
                placeholder="Filtrer les produits..."
            />
            <p>{selection.size} produit(s) sélectionné(s)</p>
            <table>
                <tbody>
                    {produitsFiltres.map(produit => (
                        <LigneTableau
                            key={produit.id}
                            item={produit}
                            estSelectionne={selection.has(produit.id)}
                            onSelectionner={handleSelectionner}
                            onSupprimer={handleSupprimer}
                        />
                    ))}
                </tbody>
            </table>
        </div>
    );
}

Dans cet exemple :

  • Quand l'utilisateur tape dans le filtre → seul produitsFiltres est recalculé, les LigneTableau non affectées ne re-rendent pas
  • Quand l'utilisateur sélectionne une ligne → seules les lignes dont estSelectionne a changé re-rendent
  • handleSelectionner et handleSupprimer gardent la même référence → aucun re-rendu inutile causé par les props fonctions

Quand NE PAS optimiser

C'est la section que la plupart des tutoriels omettent. Trop d'optimisation est aussi un problème : elle alourdit le code, augmente la complexité cognitive, et peut même réduire les performances dans certains cas.

Le coût caché de la mémorisation

// ❌ Sur-optimisation inutile et contre-productive
function ComposantSimple({ valeur }) {
    // useMemo sur un calcul trivial : la mémorisation coûte plus cher que le calcul !
    const double = useMemo(() => valeur * 2, [valeur]);

    // useCallback sur une fonction rarement appelée : coût sans bénéfice
    const afficher = useCallback(() => console.log(valeur), [valeur]);

    return <p onClick={afficher}>{double}</p>;
}

// ✅ Version simple — plus performante et plus lisible
function ComposantSimple({ valeur }) {
    const double = valeur * 2; // Multiplication : ~1 nanoseconde, pas besoin de memo
    return <p onClick={() => console.log(valeur)}>{double}</p>;
}
Contre-intuitif : useMemo et useCallback ont eux-mêmes un coût : allocation mémoire, comparaison des dépendances, stockage de la valeur précédente. Pour un calcul simple ou une fonction rarement appelée, ce coût dépasse le bénéfice.
Situation Optimiser ? Pourquoi
Composant simple, peu de props primitives ❌ Non React est déjà rapide sur les primitives
Calcul arithmétique basique ❌ Non Plus lent avec useMemo qu'sans
Composant qui re-rend rarement ❌ Non Pas de problème à résoudre
Filtrage/tri d'un tableau >500 items ✅ Oui Calcul coûteux exécuté souvent
Composant enfant dans une liste longue ✅ Oui Multiplié par N éléments = impact visible
Fonction passée à un composant mémorisé ✅ Oui Sinon memo ne sert à rien
Valeur passée dans un Context ✅ Oui Évite de re-rendre tous les consumers

La règle des 3 étapes

  • Écrire d'abord le code sans optimisation
  • Mesurer avec React Profiler pour identifier les vrais goulots
  • Optimiser uniquement là où la mesure montre un problème réel

Mesurer avec React Profiler

Avant d'appliquer une optimisation, vous devez prouver qu'il y a un problème. React DevTools fournit un Profiler intégré qui montre exactement quels composants se re-rendent, combien de fois, et combien de temps chaque rendu prend.

Utiliser React DevTools Profiler

// Étapes pour profiler votre application :
// 1. Installer React DevTools (extension Chrome / Firefox)
// 2. Ouvrir les DevTools → onglet "Profiler"
// 3. Cliquer sur "Record" (bouton rouge)
// 4. Interagir avec l'application (cliquer, taper, naviguer)
// 5. Cliquer sur "Stop"
// 6. Analyser le flame chart

// Résultat : vous verrez quels composants se re-rendent et pourquoi
// React indique la raison du re-rendu : "props changed", "state changed", "parent re-rendered"

Le composant Profiler programmatique

import { Profiler } from 'react';

// Callback appelé à chaque rendu du composant profilé
function onRenderCallback(
    id,          // identifiant du Profiler
    phase,       // "mount" ou "update"
    actualDuration, // temps réel de ce rendu (ms)
    baseDuration,   // temps estimé sans mémo
    startTime,      // quand le rendu a commencé
    commitTime      // quand React a commité les changements
) {
    // Logger les rendus lents (>16ms = drop de frame)
    if (actualDuration > 16) {
        console.warn(`[Profiler] ${id} rendu lent : ${actualDuration.toFixed(2)}ms`);
    }
}

// Envelopper le composant à profiler
function App() {
    return (
        <Profiler id="TableauProduits" onRender={onRenderCallback}>
            <TableauProduits produits={data} />
        </Profiler>
    );
}

Détecter les re-rendus inutiles manuellement

// Technique simple : utiliser un ref pour compter les rendus
import { useRef, memo } from 'react';

const ComposantSurveille = memo(function ComposantSurveille({ titre }) {
    // useRef persiste entre les rendus sans déclencher de re-rendu
    const renderCount = useRef(0);
    renderCount.current += 1;

    // En développement : vérifier que le compteur n'explose pas
    if (process.env.NODE_ENV === 'development') {
        console.log(`${titre} : rendu #${renderCount.current}`);
    }

    return <div>{titre}</div>;
});

Patterns avancés et cas réels

Optimiser un Context pour éviter les re-rendus en cascade

import { createContext, useContext, useState, useMemo, useCallback } from 'react';

// Sans optimisation : TOUS les consumers re-rendent quand n'importe quelle
// valeur du contexte change — même si ce consumer n'utilise pas cette valeur

const ThemeContext = createContext(null);

// ❌ Contexte non optimisé : valeur recréée à chaque rendu du Provider
function ThemeProviderBogue({ children }) {
    const [theme, setTheme] = useState('light');
    const [langue, setLangue] = useState('fr');

    return (
        // Nouvel objet à chaque rendu → TOUS les consumers re-rendent
        <ThemeContext.Provider value={{ theme, setTheme, langue, setLangue }}>
            {children}
        </ThemeContext.Provider>
    );
}

// ✅ Contexte optimisé : valeur mémorisée
function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');
    const [langue, setLangue] = useState('fr');

    // Mémoriser les setters (déjà stables mais bonne pratique explicite)
    const toggleTheme = useCallback(() => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    }, []);

    // Mémoriser l'objet contexte — ne change que si theme ou langue change
    const valeurContexte = useMemo(() => ({
        theme,
        langue,
        toggleTheme,
        setLangue,
    }), [theme, langue, toggleTheme]);

    return (
        <ThemeContext.Provider value={valeurContexte}>
            {children}
        </ThemeContext.Provider>
    );
}

Mémoriser des listes avec des IDs stables

// Mauvaise pratique : utiliser l'index comme key
// React peut confondre les éléments lors de suppressions/insertions
function ListeMauvaise({ items }) {
    return (
        <ul>
            {items.map((item, index) => (
                // ❌ key={index} : problèmes lors de réordonnancement
                <li key={index}>{item.nom}</li>
            ))}
        </ul>
    );
}

// Bonne pratique : ID unique et stable + memo sur le composant enfant
const ItemListe = memo(function ItemListe({ item, onEdit }) {
    return (
        <li>
            {item.nom}
            <button onClick={() => onEdit(item.id)}>Éditer</button>
        </li>
    );
});

function ListeOptimisee({ items }) {
    const handleEdit = useCallback((id) => {
        console.log(`Éditer item ${id}`);
    }, []);

    return (
        <ul>
            {items.map(item => (
                // ✅ key={item.id} : ID unique et stable
                <ItemListe
                    key={item.id}
                    item={item}
                    onEdit={handleEdit} // Référence stable grâce à useCallback
                />
            ))}
        </ul>
    );
}

Lazy initialization de useState

// Cas réel : initialiser l'état depuis localStorage (lecture coûteuse)

// ❌ Sans lazy init : lecture localStorage à CHAQUE rendu
function PreferencesSansLazy() {
    // Lire localStorage est synchrone mais coûteux sur beaucoup de rendus
    const [prefs, setPrefs] = useState(
        JSON.parse(localStorage.getItem('prefs') || '{}')
    );
}

// ✅ Avec lazy init : la fonction n'est appelée QU'AU PREMIER rendu
function PreferencesAvecLazy() {
    const [prefs, setPrefs] = useState(() => {
        // Cette fonction n'est exécutée qu'une seule fois (au montage)
        try {
            return JSON.parse(localStorage.getItem('prefs') || '{}');
        } catch {
            return {}; // Fallback si le JSON est corrompu
        }
    });
}

Checklist performance React

Récapitulatif pratique à garder sous la main lors de vos revues de code.

  • J'ai mesuré le problème avec React DevTools Profiler avant d'optimiser
  • Mes composants lourds dans des listes utilisent memo
  • Les fonctions passées à des composants mémorisés utilisent useCallback
  • Les objets passés comme props (config, options) utilisent useMemo
  • Les calculs coûteux (>500 items, parsing complexe) utilisent useMemo
  • La valeur d'un Context est mémorisée avec useMemo
  • Toutes les key dans mes listes sont des IDs stables (pas des index)
  • Les initialisations coûteuses de useState utilisent la lazy initialization
  • Je n'ai pas appliqué memo / useMemo / useCallback par précaution sans mesurer
  • Le code reste lisible — l'optimisation ne sacrifie pas la maintenabilité
Conclusion : React.memo, useMemo et useCallback sont des outils puissants mais à utiliser avec discernement. Le secret d'une application React performante n'est pas de tout mémoriser — c'est de mesurer d'abord, optimiser ensuite, et de choisir le bon outil selon le contexte. Un code simple et sans optimisation prématurée sera toujours plus maintenable qu'un code surchargé de mémorisations inutiles.

Partager