Les bases pour optimiser son application React
- Blog
- Technique
- Les bases pour optimiser son application React
Article rédigé par Léo - développeur fullstack
Quand on développe en React, arrive fatalement un moment où l’application commence à ralentir: trop de composants qui se re-rendent, des listes qui saturent le navigateur, des calculs lourds répétés à chaque render. Ces problèmes de performance ne sont pas une fatalité. Dans cet article, je partage les techniques et bonnes pratiques que j’utilise au quotidien pour identifier les goulots d’étranglement et optimiser les applications React, sans tomber dans la suroptimisation. Cela vous permettra notamment de booster votre SEO grâce aux performances gagnées.
Premières vérifications
Il est important de vérifier que la partie que vous voulez optimiser est lente également en mode production (après build), que vous n’avez pas mis un state plus haut dans l’arborescence que nécessaire.
Enfin, trouver quel composant/groupe de composant a des problèmes d’optimisation via l'extension React DevTools Profiler et les encapsuler dans un memo().
Le choix du paradigme
En JS moderne, il existe 2 paradigmes principaux concernant les boucles (qui sont généralement la cause de la lenteur de l’application): L’impératif et le déclaratif.
La différence fondamentale entre les deux est une question de lisibilité et de performances.
Le paradigme déclaratif
Le déclaratif (forEach, map, filter, reduce) est très parlant et donc très lisible. Il limite également les effets de bord. Mais ceci, au coût des performances sur des tableaux de grande taille.
Le paradigme impératif
L’impératif (for, for … of) est quelques fois moins parlant et peut créer des effets de bords. Cependant, il reste le paradigme le plus efficace sur une grande quantité de données.
Quand utiliser l’impératif ou le déclaratif?
|
Situation |
A préférer |
Pourquoi |
Exemple simple |
|---|---|---|---|
|
Transformations pures simples |
Déclaratif (map, filter, reduce) |
Lisible, composable, pas d’état mutable |
x.filter(f).map(m) |
|
Chaînes lisibles (de taille modeste) |
Déclaratif |
Intention facile à lire |
users.map(u => u.id) |
|
Effets de bord (logs, mutation) |
Impératif |
Contrôle explicit, pas de surprises |
for (const x of a) log(x) |
|
Early-exit générique |
Impératif |
break/continue/return simples |
for (...) { if (ok) break } |
|
Early-exit à priori |
Déclaratif (some, find, every) |
Stop avant la boucle |
if (a.some(x)) return |
|
Très gros volumes |
Impératif |
Une seule passe, moins d’allocations |
for (const x of a) { … } |
|
Besoin d’index / compteur précis |
Impératif (ou entries()) |
Sauts, pas irréguliers, fenêtres |
for (let i=0; i<n; i++) … |
|
Async séquentiel (par élément) |
Impératif (for … of + await) |
await fonctionne |
for (const x of a) await f(x) |
|
Async parallèle |
Déclaratif (map + promise.all) |
Simple et lisible (attention aux quotas) |
await Promise.all( a.map(f)) |
|
Gestion d’erreur fine par item |
Impératif |
try/catch par itération clair |
for (...) { try { … } catch { … } } |
|
Immuabilité |
Déclaratif |
Pas de mutation partagée |
a.reduce(acc, init) |
|
Itérables non-tableaux (Map/Set/Générateurs) |
Impératif (for … of) |
Naturel, sans conversion |
for (const x of set) ... |
L’adaptation du code
Il existe des méthodes pour optimiser le code comme le useMemo par exemple. Cependant, l’utilisation de ce hook peut être évitée en changeant l’architecture du code pour produire une structure plus adaptée à la logique.
Le changement d’architecture
Cette partie sera inspirée par cet article, écrit par Dan Abramov, un des contributeurs majeurs de ReactJS.
Il y évoque la problématique suivante:
import { useState } from 'react';
export default function App() {
let [color, setColor] = useState('red');
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
function ExpensiveTree() {
let now = performance.now();
while (performance.now() - now < 100) {
// Artificial delay -- do nothing for 100ms
}
return <p>I am a very slow component tree.</p>;
}
Le problème dans ce composant réside dans le fait que le changement du state color provoquera un re-render du composant <ExpensiveTree>, qui pourtant n’est pas lié à la logique de la couleur.
Il propose plusieurs solutions pour pallier ce problème sans recourir à l’utilisation du useMemo().
1ère solution: Descendre le state
Comme évoqué précédemment, la logique du changement de couleur ainsi que ses répercutions n’ont pas de lien avec <ExpensiveTree>. Donc la première solution logique serait de faire redescendre le state afin d’isoler la logique du changement de couleur ainsi que ses répercutions dans un composant à part.
Ceci fait, nous obtenons ce résultat:
export default function App() {
return (
<>
<Form />
<ExpensiveTree />
</>
);
}
function Form() {
let [color, setColor] = useState('red');
return (
<>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
</>
);
}
Le résultat obtenu permet de re-render le composant <Form> à chaque changement de couleur dans le state, sans re-render <ExpensiveTree> pour autant.
Cependant, cette solution ne fonctionne pas dans les cas où le state est utilisé au-dessus du composant <ExpensiveTree> dans l’arborescence de l’application. Ce qui nous amène à la seconde solution.
2nde solution: Faire monter le contenu dans le code
Imaginons le cas suivant:
export default function App() {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
Impossible de descendre le state dans ce cas de figure. La solution qui s’offre à nous est alors simple mais pas forcément évidente aux premiers abords.
Il nous suffit de créer un composant contenant la logique de changement de couleur puis de passer <ExpensiveTree> en tant que children de ce composant. Ce qui nous donne ceci:
export default function App() {
return (
<ColorPicker>
<p>Hello, world!</p>
<ExpensiveTree />
</ColorPicker>
);
}
function ColorPicker({ children }) {
let [color, setColor] = useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
}
Pourquoi ça fonctionne?
La réponse réside dans la manière dont React fonctionne. React parcourt seulement les éléments qui changent pour les re-render. Or, en mettant la logique dans un composant à part, en changeant le state, la <div> et l’<input> vont être re-render. A contrario, les children n’ont pas changé dans le composant parent, donc React ne va pas parcourir <ExpensiveTree> pour le re-render (car aucune raison de le re-render s’il ne change pas).
Les différents hooks d’optimisation
Le useMemo
Si les méthodes précédentes ne fonctionnent pas, nous pouvons toujours utiliser UseMemo(). C’est un hook de React qui permet de mettre en cache le résultat d’un calcul entre les différents re-render.
Il s’utilise de la manière suivante:
const cachedValue = useMemo(calculateValue, dependencies)
Il est très pratique dans le cas où l’on effectue un parsing ou un calcul lourd. On l’utilise par exemple souvent dans les providers de contexte React.
Exemple simple (sans useMemo, mauvaise pratique)
const MyContext = createContext();
function MyProvider({ children }) {
const value = { theme: "dark" }; // nouvel objet à chaque render
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}
Version optimisée avec useMemo
const MyContext = createContext();
function MyProvider({ children }) {
const value = useMemo(() => ({ theme: "dark" }), []); // value garde la même référence tant que rien ne change, évite les re-renders inutiles
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}
Je vous invite à lire la page de documentation dédiée à useMemo qui est très complète et remplie d’exemples interactifs.
Le useCallback et React.memo
Le hook useCallback permet de mettre en cache la définition de fonctions. Cela peut être utile dans plusieurs cas: L’optimisation de composant avec une fonction passée en props, éviter une boucle infinie lors de l’utilisation de fonctions dans un useEffect déclarées en dehors de ce dernier, ou l’optimisation d’un custom hook.
Nous allons étudier un exemple où le useCallback est utilisé à des fins d’optimisation de composants.
Supposons le code suivant:
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
};
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
On se rend compte alors que lors du changement du thème, l’application se fige un instant mais que si l’on retire le composant <ShippingForm>, tout se passe bien. Cela signifie que ce dernier a besoin d’être optimisé (car la mise à jour d’un composant provoque récursivement le re-render des composants inclus dans le composant).
Cela peut ainsi vous rappeler la 2nde solution que nous avons vu et en effet, nous pourrions passer <ShippingForm> en children et remonter la logique dans le composant parent.
Cependant, imaginons le cas où nous souhaiterions explicitement garder la logique du handleSubmit dans ce component, comment faire?
Dans ce cas, il faut indiquer explicitement à React que nous souhaitons mettre en cache le composant <ShippingForm>, tant que les props ne changent pas, en utilisant la fonction React.memo de cette façon:
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
Cependant, en JavaScript, la déclaration de la fonction handleSubmit comme nous l’avons fait va créer une nouvelle instance de cette fonction à chaque re-render. Qui dit nouvelle instance, dit React va détecter un changement dans les props du composant <ShippingForm> et donc va le re-render même si visuellement, pour nous, la fonction est la même. C’est là qu’intervient useCallback. Pour que notre optimisation via React.memo fonctionne, nous devons mettre en cache la fonction handleSubmit pour qu’elle reste la même, indépendamment du re-render de son composant.
Ce qui nous donne ce résultat optimisé:
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
La virtualisation des listes
Le problème est le suivant: lorsqu’on veut afficher une liste gigantesque (exemple: 10k items dans un map), React va monter/render tous les éléments en mémoire (même ceux hors écrans) et donc cela impacte grandement les performances.
La virtualisation vient résoudre ce problème: render uniquement ce qui est visible à l’écran.
Le fonctionnement est simple à comprendre, on scroll dans le tableau mais seulement les éléments à l’écran ainsi qu’un petit buffer sont render. Puis lors du scroll, les éléments hors champ sont recyclés, économisant ainsi la mémoire et le CPU.
Des librairies existent pour réaliser cela comme react-window et react-virtualized.
Exemple avec react-window:
import { List } from "react-window";
function Example({ names }: { names: string[] }) {
return (
<List
rowComponent={RowComponent}
rowCount={names.length}
rowHeight={25}
rowProps={{ names }}
/>
);
}
La segmentation de code
La segmentation de code est une partie importante de l’optimisation notamment lors de l’utilisation de fichiers lourds. C’est ce que permet de faire React via React.lazy, qui va charger la fonction passée en paramètre seulement lorsque le code va être utilisé (par exemple dans un react-router où l’on ne veut charger que la page en cours et non toutes les pages).
Exemple simple:
import { Suspense, lazy } from 'react';
import Loading from './Loading.jsx';
const ExpensiveComponent = lazy(() => import('./ExpensiveComponent.jsx'));
const MainComponent = () => {
return (
<div>
<h1>Title</h1>
<Suspense fallback={<Loading />}>
<ExpensiveComponent />
</Suspense>
</div>
);
};
Le piège à éviter
Lorsqu’on a connaissance des techniques d’optimisation, un réflexe humain que nous avons est de vouloir les utiliser partout, certaines fois, au détriment de la lisibilité du code. Nous tombons alors dans ce qu’on appelle “l’over engineering”, la suroptimisation. Le code que nous produisons doit par essence rester lisible et compréhensible par les autres/futurs membres du projet. Certes il faut coder de manière efficiente mais il faut trouver ce juste milieu entre code lent et code suroptimisé sans réelle raison (par exemple ne pas mettre de useMemo si le calcul n’est pas coûteux).
La checklist des bases à vérifier pour optimiser son application
- Profiler avec React DevTools : identifier les vrais goulots d’étranglement avant d’optimiser.
- State au bon niveau : descendre ou isoler le state pour limiter les re-renders en cascade.
- Découpage de composants : préférer plusieurs petits composants ciblés plutôt qu’un gros qui re-render tout.
- React.memo : mémoriser les composants qui reçoivent souvent les mêmes props.
- useCallback & useMemo : stabiliser fonctions/valeurs coûteuses ou passées en props.
- Contextes légers : éviter d’y mettre trop de données. Découper ou utiliser une state lib (zustand, jotai, redux-toolkit).
- Virtualisation de listes : utiliser react-window/react-virtualized pour les grandes listes.
- Code splitting : charger le code à la demande (React.lazy, Suspense).
- Éviter le travail inutile : pas de re-calculs lourds dans le render, ni d’effets inutiles dans useEffect.
- Lisibilité > micro-optimisations : n'optimiser que ce qui impacte vraiment l’expérience utilisateur.
Besoin d'aide pour optimiser votre application ?
À la une
Découvrir plus de workshop technologiques
-
4 Février 2026
11:40 - 12:00
-
27 Janvier 2026
09:30 - 10:00
-
27 Janvier 2026
09:30 - 10:00
-
27 Janvier 2026
09:30 - 10:00
Pagination

en France






