Les bases pour optimiser son application React

- Publié 20/01/2026 - 14:59, mis à jour à 23/01/2026 - 08:33 Technique

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

  1. Profiler avec React DevTools : identifier les vrais goulots d’étranglement avant d’optimiser.
  2. State au bon niveau : descendre ou isoler le state pour limiter les re-renders en cascade.
  3. Découpage de composants : préférer plusieurs petits composants ciblés plutôt qu’un gros qui re-render tout.
  4. React.memo : mémoriser les composants qui reçoivent souvent les mêmes props.
  5. useCallback & useMemo : stabiliser fonctions/valeurs coûteuses ou passées en props.
  6. Contextes légers : éviter d’y mettre trop de données. Découper ou utiliser une state lib (zustand, jotai, redux-toolkit).
  7. Virtualisation de listes : utiliser react-window/react-virtualized pour les grandes listes.
  8. Code splitting : charger le code à la demande (React.lazy, Suspense).
  9. Éviter le travail inutile : pas de re-calculs lourds dans le render, ni d’effets inutiles dans useEffect.
  10. Lisibilité > micro-optimisations : n'optimiser que ce qui impacte vraiment l’expérience utilisateur.

 

Besoin d'aide pour optimiser votre application ?

> Contactez dès maintenant l'un de nos experts Drupal

Partagez toute l'actualité

Partagez sur Facebook Partagez sur Twitter Copier le lien

À la une

Découvrir plus de workshop technologiques

Image
Actency - Réassurance  - 7 Agences et Bureaux en France
7 Agences & Bureaux
en France
150 Experts
Image
Actency - Réassurance  - 150 experts
+1 200 Projets
Image
Actency - Réassurance - Contributeur et conférencier Drupal en Europe
Contributeur Et conférencier Drupal en Europe
11 500 Jours/hommes par an
Image
Actency - Réassurance - 11500 jours hommes par an
Nous contribuons aux évolutions et aux conférences Drupal en Europe
Image
Actency - Drupal - DrupalCon
Image
Actency - Événements - Paris OpenSource Summit
Image
Actency - Événements - IT & IT Security Meetings
Image
Actency - Événements - DrupalCamp 2020