Front-end angularforall.com

- 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.

Partager