13

Performance

Optimiser les rendus avec memo, useMemo, useCallback

Le concept : La beauté de React est de se rafraîchir à chaque changement de State. Mais face à des listes géantes ou des calculs lourds, ce re-rendu automatique peut ralentir l'application.

C'est là qu'interviennent les outils de mémorisation. React.memo agit comme un videur de boîte de nuit : "Ce composant ne se re-dessine QUE si l'on change ses Props exactes". useMemo et useCallback font la même chose, mais respectivement pour un calcul lourd ou une fonction, en les mettant en cache entre deux rendus.

Composant Parent (Re-rendu) Enfant Enfant .memo() STOP
💡 Comprendre le cycle de rendu React

Par défaut, quand un composant parent se re-rend, tous ses enfants se re-rendent aussi, même si leurs props n'ont pas changé. React fait ça parce que c'est généralement rapide — le Virtual DOM diffing est efficace.

Mais dans certains cas (grandes listes, calculs coûteux, descendance profonde), il faut optimiser. Les 3 outils principaux :

  • React.memo — Mémoiser un composant (éviter le re-rendu si les props n'ont pas changé).
  • useMemo — Mémoiser la valeur d'un calcul coûteux.
  • useCallback — Mémoiser une fonction (pour éviter de recréer une référence à chaque rendu).
⚠️ Règle de performance : N'optimisez pas prématurément. Mesurez avec les DevTools React avant d'ajouter memo/useMemo. Une sur-optimisation rend le code plus complexe sans bénéfice réel.

🧠 React.memo — Mémoisation de composant

Sans React.memo (re-rendu inutile)
// ❌ ChartBar se re-rend à chaque rendu de App
// même si ses props (value, label) n'ont pas changé
function App() {
    const [count, setCount] = useState(0);
    return (
        <>
            <button onClick={() => setCount(c => c+1)}>+</button>
            // ↓ Re-rendu inutile si value et label ne changent pas
            <HeavyChart data={bigDataset} />
        </>
    );
}
Avec React.memo ✅
// ✅ HeavyChart ne re-rend que si ses props changent
const HeavyChart = React.memo(function HeavyChart({ data }) {
    // Rendu coûteux
    return <canvas id="chart" />;
});

// Comparaison personnalisée (deep equal)
const MemoList = React.memo(MyList, (prevProps, nextProps) => {
    // Retourner true = pas de re-rendu
    return prevProps.items.length === nextProps.items.length;
});

🔢 useMemo — Mémoiser un calcul coûteux

useMemo pour calculs lourds
import { useMemo } from 'react';

function ProductList({ products, filter }) {
    // ❌ Recalculé à chaque rendu
    // const filtered = products.filter(p => p.category === filter);

    // ✅ Recalculé uniquement si products ou filter change
    const filtered = useMemo(() => {
        return products
            .filter(p => p.category === filter)
            .sort((a, b) => a.price - b.price);
    }, [products, filter]);

    return filtered.map(p => <ProductCard key={p.id} {... p} />);
}
useCallback — Mémoiser une fonction
import { useCallback } from 'react';

function TodoList({ items }) {
    const [selected, setSelected] = useState(null);

    // ❌ Nouvelle fonction à chaque rendu → memo échoue
    // const handleSelect = (id) => setSelected(id);

    // ✅ Même référence de fonction entre les rendus
    const handleSelect = useCallback((id) => {
        setSelected(id);
    }, []); // Stable car setSelected l'est toujours

    return items.map(item => (
        <TodoItem key={item.id} onSelect={handleSelect} />
    ));
}
const TodoItem = React.memo(({ onSelect }) => { ... });

📦 Code Splitting : lazy() & Suspense

Chargement différé de composants lourds
import { lazy, Suspense } from 'react';

// Chargement à la demande (bundle splitting Vite/Webpack)
const Dashboard  = lazy(() => import('./pages/Dashboard'));
const HeavyChart = lazy(() => import('./components/HeavyChart'));

// Suspense affiche un fallback pendant le chargement
function App() {
    return (
        <Suspense fallback={<div>Chargement...</div>}>
            <Routes>
                <Route path="/dashboard" element={<Dashboard />} />
            </Routes>
        </Suspense>
    );
}

// Impact : au lieu d'un bundle de 2MB, l'utilisateur charge
// un bundle initial de ~300KB, puis les pages à la demande

🔍 Quand optimiser ?

  • ✅ Listes de 100+ éléments avec re-rendus fréquents
  • ✅ Calculs mathématiques lourds (tri, filtrage complexe)
  • ✅ Callback passé à des enfants mémoïsés (React.memo)
  • ✅ Code splitting pour les routes et composants lourds
  • ❌ Composants simples (le memo coûte aussi quelque chose)
  • ❌ Prématurément, sans mesure préalable

🛠️ Outils de diagnostic

  • 🔧 React DevTools Profiler — Visualise quel composant se rend et combien de temps.
  • ⚙️ why-did-you-render — Détecte les re-rendus inutiles en dev.
  • 📊 Lighthouse — Métriques de performance globales.
  • 🔍 React DevTools → Components → Mettre en surbrillance les composants qui re-rendent.
💡 Mémorisation vs Virtualisation Pour les très longues listes (1000+ éléments), la mémoisation ne suffit pas. Utilisez la virtualisation : n'afficher que les éléments visibles dans la fenêtre avec react-window ou @tanstack/react-virtual.