Front-end

React memo, useMemo, useCallback : performance

React React-Memo Usememo Usecallback Useref React-Compiler Memoisation Shallow-Comparison React-Window Tanstack-Virtual Performance Devtools-Profiler
React memo, useMemo, useCallback : performance

React memo, useMemo, useCallback : memoisation, shallow comparison, React Compiler, virtualisation react-window, DevTools Profiler et anti-patterns.

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.

React Compiler — l'avenir de la mémoïsation automatique

React Compiler (anciennement React Forget) est un compilateur officiel qui analyse votre code React et insère automatiquement les optimisations de mémoïsation au build time. Stable depuis fin 2024, intégré nativement à Next.js 15+ et disponible en plugin Babel/SWC pour Vite et autres bundlers.

// Code que vous écrivez — aucun useMemo manuel
function ProductList({ products, query }) {
    const filtered = products.filter(p => p.name.includes(query));
    const total = filtered.reduce((sum, p) => sum + p.price, 0);
    const handleAdd = (id) => addToCart(id);

    return (
        <>
            <Total amount={total} />
            {filtered.map(p => <Card key={p.id} product={p} onAdd={handleAdd} />)}
        </>
    );
}

// Code généré par React Compiler — mémoïsations injectées
function ProductList({ products, query }) {
    const $ = useMemoCache(4);
    const filtered = $.has(0, products, query)
        ? $.get(0)
        : $.set(0, products.filter(p => p.name.includes(query)), [products, query]);
    const total = $.has(1, filtered)
        ? $.get(1)
        : $.set(1, filtered.reduce((s, p) => s + p.price, 0), [filtered]);
    const handleAdd = $.has(2)
        ? $.get(2)
        : $.set(2, (id) => addToCart(id), []);
    // ...
}

Activer React Compiler

// next.config.js (Next.js 15+)
const nextConfig = {
    experimental: {
        reactCompiler: true,
    },
};

// Avec Vite — plugin Babel
// vite.config.ts
import react from '@vitejs/plugin-react';
export default defineConfig({
    plugins: [react({ babel: { plugins: ['babel-plugin-react-compiler'] } })],
});

Règles à respecter pour que le compilateur fonctionne

  • Pas de mutation — modifier state.x = y au lieu de setState({...state, x: y}) casse l'analyse. Utiliser immer ou spread.
  • Pas de side-effects dans le rendu — pas de console.log(), pas de fetch() en dehors de useEffect.
  • Règles des hooks respectées — pas d'appel conditionnel, dans la bonne position.
  • Composants purs — même props = même output. C'est la condition fondamentale de la mémoïsation.

Le compilateur ajoute un opt-in "use no memo" en tête de fichier pour désactiver l'optimisation localement si nécessaire. Pour les nouveaux projets en 2026, activez React Compiler dès le départ — vous économisez 70-80 % du code de mémoïsation manuel sans perdre les bénéfices de performance.

Patterns avancés pour les listes virtualisées

Sur des listes de 10 000+ éléments, même avec memo, le coût de rendu reste prohibitif. La virtualisation ne rend que les éléments visibles dans le viewport — typiquement 15-30 sur un écran standard.

// react-window — virtualisation simple
import { FixedSizeList } from 'react-window';

function VirtualUserList({ users }: { users: User[] }) {
    return (
        <FixedSizeList
            height={600}
            itemCount={users.length}
            itemSize={64}
            width="100%">
            {({ index, style }) => (
                <div style={style}>
                    <UserCard user={users[index]} />
                </div>
            )}
        </FixedSizeList>
    );
}

// Pour des hauteurs variables — VariableSizeList ou TanStack Virtual
import { useVirtualizer } from '@tanstack/react-virtual';

Avec 10 000 éléments, le temps de rendu initial passe de 800ms (rendu complet) à 12ms (virtualisé). TanStack Virtual (anciennement React Virtual) est le successeur moderne — supporte les hauteurs variables, le scroll horizontal et la grille à colonnes multiples, avec une API identique en React/Vue/Solid.

Mini-projet appliqué — audit perf avant/après mémoïsation

Cas concret : une table de produits avec filtrage en temps réel sur 2 000 lignes. Avant optimisation, chaque frappe dans le filtre re-rend la table entière (240 ms par frappe sur Pixel 5). Voici la démarche en 4 étapes pour mesurer puis optimiser.

1. Code initial — naïf et lent

function ProductsTable({ products }) {
    const [filter, setFilter] = useState('');

    // ❌ Filtre recalculé à chaque render — même quand `filter` est inchangé
    const filtered = products.filter(p =>
        p.name.toLowerCase().includes(filter.toLowerCase())
    );

    // ❌ Nouvelle fonction à chaque render — casse memo() en aval
    const handleDelete = (id) => deleteProduct(id);

    return (
        <>
            <input value={filter} onChange={(e) => setFilter(e.target.value)} />
            {filtered.map(p => (
                <ProductRow key={p.id} product={p} onDelete={handleDelete} />
            ))}
        </>
    );
}

2. Mesure baseline avec React Profiler

Pour le détail du Profiler et de Web Vitals, voir le guide React 18 rendu concurrent.

// Wrap dans <Profiler> pour mesurer
import { Profiler } from 'react';

<Profiler id="ProductsTable" onRender={(id, phase, actualDuration) => {
    console.log(`${id} (${phase}): ${actualDuration.toFixed(1)} ms`);
}}>
    <ProductsTable products={products} />
</Profiler>

// Résultats baseline (mesuré sur Pixel 5, throttling Chrome 6x) :
// ProductsTable (mount) : 320 ms
// ProductsTable (update) : 240 ms par frappe → INP catastrophique

3. Optimisations ciblées en 3 mouvements

function ProductsTable({ products }) {
    const [filter, setFilter] = useState('');

    // ✓ Filtrage mémoïsé — recalculé seulement si products ou filter change
    const filtered = useMemo(() => {
        const lower = filter.toLowerCase();
        return products.filter(p => p.name.toLowerCase().includes(lower));
    }, [products, filter]);

    // ✓ Callback stable — memo() de ProductRow fonctionne désormais
    const handleDelete = useCallback((id: string) => deleteProduct(id), []);

    return (
        <>
            <input value={filter} onChange={(e) => setFilter(e.target.value)} />
            {filtered.map(p => (
                <ProductRow key={p.id} product={p} onDelete={handleDelete} />
            ))}
        </>
    );
}

// ✓ ProductRow mémoïsé — skip si props inchangées
const ProductRow = memo(function ProductRow({ product, onDelete }: Props) {
    return (
        <tr>
            <td>{product.name}</td>
            <td>{product.price}€</td>
            <td><button onClick={() => onDelete(product.id)}>Suppr</button></td>
        </tr>
    );
});

4. Mesure finale + gain mesurable

Gains mesurés en production (Pixel 5, throttling Chrome 6x) :
  • Mount : 320 ms → 110 ms (−66 %)
  • Update par frappe : 240 ms → 18 ms (−93 %)
  • INP médian : 450 ms → 70 ms (passe de "Poor" à "Good")
  • Re-renders ProductRow : 2 000 par frappe → ~0 (seules les rows ajoutées/retirées du filtre re-render)
Coût d'optimisation : 3 lignes ajoutées (useMemo + useCallback + memo wrapper). ROI : visible immédiatement.

5. Aller plus loin avec la virtualisation

Pour des listes > 5 000 éléments, même les optimisations memo deviennent insuffisantes. La virtualisation (voir section dédiée) ne rend que ~20 éléments visibles dans le viewport, ce qui passe le temps de mount sous 12 ms quel que soit le total. C'est ce que font Linear, Notion, et toutes les apps qui affichent des milliers d'éléments. Pour le data fetching associé, voir le guide TanStack Query qui combine virtualisation + pagination cursor automatique.

Questions fréquentes

React.memo est un Higher-Order Component qui mémoïse un composant entier : si ses props n'ont pas changé (comparaison superficielle), React réutilise le rendu précédent sans re-rendre. useMemo est un hook qui mémoïse le résultat d'un calcul coûteux à l'intérieur d'un composant : const filtered = useMemo(() => expensiveFilter(data), [data]) ne recalcule que quand data change. useCallback mémoïse une fonction elle-même : const handleClick = useCallback(() => doSomething(id), [id]) retourne la même référence de fonction entre les rendus tant que id ne change pas. La règle pratique : useCallback est utile principalement quand la fonction est passée comme prop à un composant React.memo — sinon la mémoïsation du composant enfant est annulée.

React.memo échoue à prévenir les re-rendus quand les props passées sont des objets ou fonctions créés inline : recrée un nouvel objet style à chaque rendu du parent, invalidant la mémoïsation. De même, un callback défini directement dans le JSX change de référence à chaque rendu. React.memo n'est pas gratuit : chaque rendu du parent déclenche une comparaison de toutes les props, ce qui a un coût CPU. Sur des composants avec peu de props simples et un rendu rapide, React.memo peut être plus lent que le re-rendu direct. Utiliser React DevTools Profiler pour mesurer avant d'ajouter memo — la règle d'or est "mesurer d'abord, optimiser ensuite".

React DevTools Profiler permet d'enregistrer une session d'interaction et de visualiser quels composants se re-rendent, combien de fois et pourquoi. Les composants avec une flamme jaune/rouge et un temps de rendu élevé sont les candidats prioritaires. L'option "Highlight updates when components render" affiche un flash visuel sur chaque re-rendu — si des composants inattendus flashent lors d'une interaction simple, il y a une optimisation possible. Le panneau "Why did this render?" (en mode développement) liste précisément quelles props ou états ont changé. Commencer par les composants rendus dans des listes (chaque item re-rendu = n re-rendus) et ceux avec des calculs lourds visibles via la prop drilling.

La surutilisation est un anti-pattern courant : envelopper tout dans useMemo/useCallback alourdit le code et peut même dégrader les performances à cause du coût de mémoïsation. La règle de Kent C. Dodds (mainteneur React Testing Library) : n'utiliser useMemo que pour les calculs avec une complexité mesurable (O(n²) sur de grands tableaux, expressions régulières complexes) et useCallback uniquement quand la fonction est une dépendance de useEffect ou prop d'un composant mémoïsé. Le compilateur React (React Compiler, anciennement React Forget), disponible en bêta avec Next.js 14+, applique automatiquement ces optimisations sans annotations manuelles, rendant ces hooks manuels largement obsolètes dans les projets qui l'adoptent.

React.memo compare chaque prop avec Object.is() : les primitives (string, number, boolean) sont comparées par valeur, les objets et fonctions par référence. Si user.name change mais user reste la même référence d'objet, memo considère que rien n'a changé et skip le re-rendu — bug subtil si on mute un objet au lieu de créer une nouvelle référence. Pour personnaliser la comparaison, passez un deuxième argument à memo : React.memo(MyComponent, (prev, next) => prev.user.id === next.user.id). Évitez les comparaisons profondes (deep equal) — elles annulent souvent le gain de performance via leur propre coût CPU.

useMemo recalcule la valeur quand les dépendances changent — utile pour les valeurs dérivées de props/state. useRef stocke une valeur mutable qui persiste entre les rendus SANS déclencher de re-rendu lorsqu'elle change — utile pour les références DOM, timer IDs, ou valeurs "instance" du composant. Anti-pattern courant : utiliser useMemo([]) pour créer un objet stable une fois — utilisez useRef à la place (const ref = useRef(initialValue)). useRef n'a pas de tableau de dépendances et garantit la même référence pour toute la vie du composant.

Oui en grande partie. React Compiler (stable depuis fin 2024) analyse le code et insère automatiquement les optimisations de mémoïsation au build time. Vous écrivez const filtered = items.filter(x => x.active) sans useMemo — le compilateur détecte le calcul coûteux et la stabilité référentielle nécessaire, et génère le code mémoïsé équivalent. Les hooks manuels restent utiles dans 3 cas : (1) projets sans React Compiler, (2) optimisations très ciblées que le compiler ne détecte pas, (3) cas où vous voulez forcer une référence stable pour une lib externe. Pour la majorité du code applicatif moderne, n'écrivez plus useMemo/useCallback à la main.

Trois outils complémentaires. (1) web-vitals lib : import { onINP } from "web-vitals"; onINP(metric => console.log(metric.value)). Mesure réelle dans le navigateur de l'utilisateur, peut être envoyé à votre analytics. (2) Chrome DevTools Performance panel : enregistrer une session d'interaction, l'INP s'affiche dans le panel "Interactions". Permet de zoomer sur les long tasks responsables. (3) PageSpeed Insights / Lighthouse : INP mesurée en lab + données CrUX (Chrome User Experience) sur les 28 derniers jours. Cibles : < 200 ms = Good, 200-500 ms = Needs Improvement, > 500 ms = Poor. En 2026, INP est une métrique Core Web Vitals qui impact directement le SEO Google.

Règle quantitative simple. (1) < 100 items : ni memo ni virtualisation — code direct. (2) 100-1000 items : React.memo sur les items + useMemo sur le filtrage. Mount ~50 ms, fluide. (3) 1000-10 000 items : virtualisation OBLIGATOIRE (TanStack Virtual, react-window). Memo seul reste lent au mount. (4) > 10 000 items : virtualisation + pagination cursor + lazy loading. Voir TanStack Query infinite queries. La virtualisation ne rend QUE les ~20 items visibles dans le viewport — temps de rendu indépendant du total. Coût : layout fixe (height/itemSize) ou estimation pour hauteurs variables. Pour Linear, Notion, Slack qui affichent des milliers de lignes, c'est INCONTOURNABLE.

Cinq causes courantes. (1) Dépendance qui change de référence à chaque render (objet inline, fonction inline parent) → useMemo recalcule à chaque fois. Solution : remonter la dépendance dans le parent avec useMemo/useCallback. (2) Le calcul est SI peu coûteux que le coût du compare des deps dépasse le gain. Mesurer avec Profiler avant. (3) Vous utilisez la valeur dans JSX avec un objet wrapping (style={{ width: memoized }}) — l'objet style est recréé. Solution : memo() le composant enfant. (4) Le useMemo wrap une création d'objet trivial (const obj = useMemo(() => ({ x: 1 }), [])) — autant utiliser useRef. (5) Tableau de deps mal déclaré → ESLint exhaustive-deps doit être activé en strict mode.

Partager