Écrit par Thomas M.
Diagnostiquer, comprendre et optimiser les performances d’une application React
Une application React “lente” ne se résume pas à “React est lourd”. Dans la majorité des cas, les problèmes viennent de re-renders inutiles, d’un DOM trop volumineux ou d’un bundle surdimensionné. L’objectif de cet article est de donner une démarche concrète et des techniques pratiques pour diagnostiquer et optimiser les performances de vos composants.
Comprendre d’où viennent les lenteurs
Chaque mise à jour en React passe par le cycle render → reconciliation → commit : React exécute le composant, compare le virtual DOM avec le précédent puis applique les changements au DOM. Un re-render peut venir d’un setState, d’un changement de props, de context, ou simplement d’un parent qui se rafraîchit.
Les ralentissements proviennent souvent de re-renders inutiles, notamment lorsque :
- Un composant se re-render alors que ses props n’ont pas changé,
- Des callbacks ou objets sont recréés à chaque render (
() => …,style={{…}}), - Un state ou un contexte trop global force une grande partie de l’arbre à se mettre à jour.
Identifier ces situations permet de cibler précisément ce qu’il faut optimiser avant d’appliquer React.memo, useCallback ou useMemo.
Profiler l’application pour mesurer avant d’optimiser
- Le React DevTools Profiler est l’outil principal pour analyser les re-renders.
Il permet d’enregistrer une interaction et d’inspecter :- Le Flamegraph : visualise les composants les plus coûteux (barres larges ou colorées).
- La vue Ranked : classe les composants du plus lent au plus rapide.
- “Why did this render?” : indique la raison exacte d’un re-render (
props changed,parent rendered, etc.).
Exemple classique : une simple frappe dans un champ de recherche rafraîchit un Header statique → signe d’un re-render inutile.
- Quand l’UI freeze malgré des renders React rapides, utilisez l’onglet Performance de Chrome pour repérer des Long Tasks (>50 ms) sur le thread principal. On y détecte souvent des :
- Calculs JavaScript lourds,
- Recalculs de layout,
- Phases de garbage collection.
Croiser les données des deux outils permet de savoir si le problème vient réellement de React ou d’un coût externe (algorithme, librairie, manipulation du DOM).
Limiter les re-renders avec React.memo, useMemo, useCallback
React.memo : limiter le render d’un composant
React.memo mémorise le render d’un composant fonctionnel basé sur une comparaison superficielle de ses props.
const Header = React.memo(function Header({ user }) {
return <h1>Bonjour {user.name}</h1>;
});
Si user (référence) ne change pas, Header ne se re-render pas, même si son parent s’actualise.
Attention : si vous passez un objet ou une fonction recréés à chaque render (onClick={() => …}, style={{ ... }}), la comparaison échoue et React.memo ne sert plus à rien.
React.memo effectue une comparaison superficielle (shallow compare) des props à chaque render du parent.
Cette comparaison a elle-même un coût, proportionnel au nombre de props. Sur des composants simples ou peu coûteux à rendre, ce coût peut être supérieur au gain, rendant React.memo inutile voire contre-productif.
useMemo : Mémoïser les calculs et stabiliser les objets
Deux grands cas d’usage :
- Calculs coûteux
const filteredTodos = useMemo(
() => todos.filter(t => t.text.includes(filter)),
[todos, filter]
);
Le filtre n’est recalculé que si todos ou filter change réellement.
- Stabiliser des props non primitives
const style = useMemo(
() => ({ backgroundColor: theme === 'dark' ? 'black' : 'white' }),
[theme]
);
// sans useMemo, un nouveau `{}` à chaque render casse React.memo
<MemoizedChild style={style} />;
useCallback : Stabiliser les fonctions passées en props
useCallback est l’équivalent de useMemo pour les fonctions :
const handleClick = useCallback(() => {
alert('Clic !');
}, []);
<MemoizedButton onClick={handleClick} />;
Indispensable dès que :
- Vous passez un callback à un composant mémoïsé,
- Vous utilisez une fonction dans les dépendances d’un
useEffect.
Colocaliser l’état et structurer l’architecture
Une des optimisations les plus simples mais tout autant efficace est de déplacer l’état aussi bas que possible.
Dans cet exemple, le composant App re-render tout à chaque saisie.
function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<HeavyChart />
<ExpensiveTable />
</>
);
}
Déplacer l’état dans un composant → la saisie ne re-render que le petit composant.
function SearchInput() {
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}
function App() {
return (
<>
<SearchInput />
<HeavyChart />
<ExpensiveTable />
</>
);
}
Dans vos formulaires, listes et dashboards :
- Séparez les états qui changent souvent (input, filtres) de ceux qui changent rarement,
- Découpez en petits composants pour limiter la zone impactée par chaque mise à jour.
Virtualiser Les listes longues
Le problème du DOM trop volumineux
Render une liste de 10 000 éléments avec un simple .map() crée 10 000 nœuds DOM. Conséquences :
- Temps de render initial énorme,
- Scroll saccadé,
- Re-renders catastrophiques,
- Risque de crash de l’onglet.
Virtualisation
La virtualisation permet de ne render que les éléments visibles à l’écran + un petit buffer. Bibliothèques courantes :
- TanStack Virtual (
@tanstack/react-virtual), - react-window.
Exemple simplifié avec TanStack Virtual :
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedList({ items }) {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div
style={{
height: rowVirtualizer.getTotalSize(),
position: 'relative',
width: '100%',
}}
>
{rowVirtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
);
}
Pour les hauteurs dynamiques, on peut mesurer chaque élément après le render et laisser la lib ajuster la position et la hauteur totale.
Optimiser le chargement et le bundle
Réduire le temps du premier chargement est tout aussi important que d’accélérer les renders.
Code splitting avec React.lazy et Suspense
Une première approche consiste à utiliser le code splitting grâce à React.lazy et Suspense. Cela permet de ne charger certaines parties qu’au moment où elles sont nécessaires, par exemple une page lourde ou une fonctionnalité secondaire.
const DashboardPage = React.lazy(() => import('./pages/DashboardPage'));
<Suspense fallback={<div>Chargement...</div>}>
<Routes>
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</Suspense>
React.lazy est particulièrement utile au niveau des routes, mais aussi pour des composants rarement affichés (éditeur riche, graphiques, modales complexes).
Lazy loading des librairies lourdes
Il est également important de pratiquer le lazy loading des librairies lourdes, en utilisant les imports dynamiques (import()). Cela évite de charger des outils coûteux tant que l’utilisateur n’en a pas besoin.
Analyse de bundle
Pour comprendre ce qui alourdit réellement l’application, il faut analyser le bundle à l’aide d’outils comme webpack-bundle-analyzer ou rollup-plugin-visualizer. Cela permet d’identifier facilement :
- Les dépendances trop volumineuses,
- Les doublons,
- Ou les imports non ciblés (ex. importer toute une bibliothèque alors qu’une seule fonction est utilisée).
Optimisation des images
Enfin, les images peuvent fortement dégrader le chargement si elles sont mal optimisées. Utiliser des formats modernes comme WebP ou AVIF, combiner cela avec loading="lazy" et ajuster la taille des fichiers permet d’obtenir un gain de performance immédiat.
Lorsque les optimisations classiques ne suffisent pas, il existe des méthodes plus avancées en fonction des cas d’utilisations.
Appliquer des techniques avancées
Quand les optimisations “classiques” ne suffisent pas :
useTransitionetuseDeferredValue: marquer certaines mises à jour comme non urgentes pour éviter les freezes lors de filtres complexes ou de gros changements d’écran. (React 18+)- Render concurrent : permet à React d’interrompre un render long pour traiter une interaction plus urgente (frappe clavier, clic).
- Web Workers : déporter les calculs CPU lourds (algos, parsing) hors du thread principal.
<canvas>/ WebGL : pour les interfaces à très haute fréquence de rafraîchissement (graphiques temps réel, visualisations interactives).
Conclusion
Optimiser les performances d’une application React, ce n’est pas tout envelopper dans React.memo.
- Mesurer avec React DevTools et le Performance Profiler.
- Identifier les re-renders inutiles et les composants réellement coûteux.
- Optimiser localement avec
React.memo,useMemo,useCallbacket une bonne colocation de l’état. - Traiter les cas extrêmes (listes longues, bundles volumineux) avec la virtualisation et le code splitting.
- Répéter le cycle mesure → optimisation → mesure.
En gardant cette démarche scientifique, vous évitez l’optimisation prématurée tout en construisant des applications React rapides, fluides et maintenables.
À lire aussi
Ouicommit Paris – IA : de l’expérimentation à l’industrialisation
Ouidou et Scaleway : un partenariat stratégique pour accélérer le cloud souverain en Europe
JTE : un moteur de templates moderne, rapide et sécurisé pour Java

