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.
| 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>
);
}
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>
);
}
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>
);
}
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
produitsFiltresest recalculé, lesLigneTableaunon affectées ne re-rendent pas - Quand l'utilisateur sélectionne une ligne → seules les lignes dont
estSelectionnea changé re-rendent handleSelectionnerethandleSupprimergardent 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>;
}
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
keydans mes listes sont des IDs stables (pas des index) - Les initialisations coûteuses de
useStateutilisent la lazy initialization - Je n'ai pas appliqué
memo/useMemo/useCallbackpar précaution sans mesurer - Le code reste lisible — l'optimisation ne sacrifie pas la maintenabilité
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.