Passer au contenu principal

Techniques d’optimisation des performances en React

Optimiser une application React, ce n’est pas appliquer tous les hacks connus, c’est d’abord mesurer, comprendre pourquoi ça rame, puis agir là où l’impact est réel. Voici un guide pour améliorer la réactivité perçue, réduire les re-renders inutiles et livrer moins de JavaScript.

 

1. Comprendre ce qui coûte cher

React fait principalement deux choses coûteuses :

  1. Rendu (calcul de l’arbre virtuel + diff)
  2. Hydratation / mise à jour du DOM (nœuds, styles, layout, peinture)

Trois leviers majeurs :

  • Rendre moins souvent (éviter les re-renders inutiles)
  • Rendre moins de choses (virtualiser, segmenter)
  • Envoyer moins de JavaScript (code splitting, RSC/SSR, images optimisées)

En pratique, il est essentiel d’optimiser après avoir mesuré. Utilisez par exemple le Profiler de React DevTools pour identifier les véritables points de blocage. Les micro-optimisations à l’aveugle complexifient souvent le code pour des gains négligeables.

2. Réduire les re-renders inutiles

2.1. Colocaliser l’état (et non le remonter systématiquement)

La colocalisation de l’état consiste à placer chaque morceau d’état dans le composant le plus bas possible, c'est à dire là où il est effectivement utilisé.

Si un état n’a pas vocation à être partagé entre plusieurs parties de l’interface, il n’a aucune raison d’être stocké dans un composant parent ou, pire, dans un contexte global.

Ce principe complète le pattern classique "lifting state up" (remonter l’état).

Remonter un état est nécessaire lorsque plusieurs composants doivent accéder à une même source de vérité. Mais une fois ce besoin disparu, il devient souvent plus efficace de redescendre l’état au plus près de son usage. C’est ce qu’on appelle la state colocation.

Pourquoi ? Parce qu’à chaque changement d’état dans un composant parent, React doit réévaluer tous ses enfants pour déterminer lesquels doivent être re-rendus.

Plus l’état est haut dans la hiérarchie, plus la zone d’impact est large.

En revanche, un état localisé dans un sous-composant limite la portée des mises à jour : seuls les éléments réellement concernés sont réévalués, ce qui réduit le coût des re-renders et améliore la réactivité de l’application.

En somme, remontez l’état uniquement quand c’est nécessaire, et colocalisez-le dès que possible.

2.2. React.memo, useMemo, useCallback

  • React.memo(Component) évite de réexécuter le rendu d’un composant si ses props n’ont pas changé.
  • useMemo(fn, deps) mémorise un résultat de calcul coûteux.
  • useCallback(fn, deps) envoie une version mémorisée d’une fonction, dont la référence reste stable tant que ses dépendances ne changent pas.

Attention aux surcoûts : mémoriser a un prix (stockage + comparaison des dépendances). N’utilisez pas useMemo/useCallback par réflexe, mais là où un re-render évité compense ce coût.

2.3. Clés de liste instables

Évitez d’utiliser l’index du tableau comme clé dans une liste.

Si l’ordre des éléments change, React perd la correspondance entre anciens et nouveaux items, ce qui provoque des re-renders inutiles et parfois des bugs discrets (état réinitialisé, focus perdu, etc.).

{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

Utilisez plutôt une clé stable issue des données, comme un identifiant unique (id).

{items.map((item) => (
  <ListItem key={item.id} item={item} />
))}

3. Rendre moins de choses : virtualisation & segmentation

3.1. Windowing

  • Pour les longues listes, ne rendez que la fenêtre visible
  • Libs utiles : react-window, react-virtualized.
  • Bénéfices : baisse drastique des nœuds DOM, du travail de diff et du layout.

3.2. Infinite scroll

C’est une technique de chargement de données : On ne récupère (ou n’ajoute) de nouvelles données que lorsque l’utilisateur approche de la fin de la liste.

Exemple : au lieu de charger 10 000 produits d’un coup, on commence par 20, puis on en charge 20 de plus au scroll.

3.3. Découper l’UI en composants autonomes

Des composants plus petits et indépendants (props stables, état local) permettent à React de ne re-rendre que la branche concernée par un changement.

4. Envoyer moins de JavaScript : code splitting, lazy, SSR

4.1. Lazy loading

Chargez le code à la demande pour réduire le bundle initial.

const Profile = React.lazy(() => import('./Profile'));

export default function App() {
  return (
    <Suspense fallback={<div>Chargement…</div>}>
      <Profile />
    </Suspense>
  );
}

4.2. SSR / SSG / Streaming

  • SSR (Server-Side Rendering) : HTML prêt à l’emploi → Time to First Paint meilleur, mais hydratation côté client reste à payer.
  • SSG (Static Site Generation) : idéal pour contenu stable.
  • Streaming : envoie le HTML au fil de l’eau + Suspense pour hydrater progressivement → meilleure perception de vitesse et TTFB (Time To First Byte) = le temps écoulé entre la requête d’un utilisateur et la réception du premier octet de réponse du serveur.

Coût à surveiller : la taille du JS hydraté. SSR n’est pas une baguette magique si le bundle client reste massif.

4.3. React Server Components (RSC) via le framework Next.js

Les RSC permettent d’exécuter une partie des composants uniquement côté serveur, sans JS client pour ces composants-là. Résultat : moins de JavaScript à hydrater, meilleure perf perçue. À utiliser pour data-fetching et logique non interactive.

5. Pratiques React souvent mal comprises qu'il faut challenger

  • Tout remonter dans un Context → explosions de re-renders. Préférez contexts granulaires.
  • Mémoriser partout (useMemo/useCallback à outrance) → complexité + surcharge. Mémorisez là où le profilage le justifie.
  • Supposer que SSR suffit → surveillez le coût d’hydratation et la taille du JS client.