← Blog
·11 blog.minutes

Le guide complet de l'optimisation des performances React

De memo aux composants serveur — un guide pratique pour rendre votre application React plus rapide sans la sur-ingénierie.

L'optimisation des performances dans React a un probleme de reputation. Demandez a dix developpeurs comment rendre une application React plus rapide, et huit diront "useMemo et useCallback partout." Les deux restants diront "l'optimisation prematuree est la racine de tous les maux" et s'en iront. Les deux groupes ont tort, et les deux groupes livrent des applications qui sont soit inutilement complexes, soit inutilement lentes.

La realite est plus nuancee. Le modele de rendu de React est efficace pour la plupart des cas immediatement, mais il y a des modeles bien compris ou il montre ses limites. Ce guide couvre toutes les techniques importantes d'optimisation React - comment elles fonctionnent, quand elles aident et, crucialement, quand elles nuisent. L'objectif n'est pas de vous faire utiliser memo partout. C'est de vous donner un modele mental pour la performance qui rend le bon choix evident dans chaque situation.

Comprendre quand React re-rend

Avant d'optimiser quoi que ce soit, vous devez comprendre ce qui provoque un re-rendu. React re-rend un composant quand son etat change, quand son parent re-rend, ou quand une valeur de contexte qu'il consomme change. Cela semble simple, mais l'effet en cascade est la ou les problemes de performance se cachent.

Quand un composant parent re-rend, chaque enfant re-rend par defaut - meme si les props de l'enfant n'ont pas change. React fait cela car il ne peut pas savoir si l'enfant depend de l'etat du parent sans executer la fonction de rendu de l'enfant. Ce n'est pas un bug ; c'est un choix de conception qui maintient le modele de reconciliation de React simple et previsible. Mais cela signifie qu'un changement d'etat dans un composant de haut niveau peut declencher des re-rendus dans des dizaines ou des centaines de descendants.

L'intuition cle est que le re-rendu n'est pas la meme chose que la mise a jour du DOM. React compare la sortie du DOM virtuel (le JSX) avec la sortie precedente et ne commit que les differences dans le DOM reel. Un composant peut re-rendre mille fois sans une seule mutation du DOM. Le cout reside dans l'execution JavaScript - creation d'objets DOM virtuels, execution des hooks et comparaison des arbres. Pour les petits arbres de composants, ce cout est negligeable. Pour les grandes listes, les graphiques complexes ou les composants qui effectuent des calculs couteux, il s'accumule rapidement.

// Chaque frappe dans cette entree re-rend tout l'arbre
function SearchPage() {
  const [query, setQuery] = useState("");

  return (
    <div>
      <SearchInput value={query} onChange={setQuery} />
      <SearchResults query={query} />
      <Sidebar>
        <FilterPanel />
        <RecentSearches />
      </Sidebar>
    </div>
  );
}

// Sans optimisation, FilterPanel et RecentSearches
// re-rendent a chaque frappe, meme si rien de ce
// dont ils dependent n'a change.

La premiere etape de toute investigation de performance consiste a identifier si vous avez reellement un probleme. L'optimisation prematuree ajoute de la complexite sans benefice mesurable. Profilez d'abord, optimisez ensuite - mais quand vous avez identifie un goulot d'etranglement, les techniques de ce guide sont les outils auxquels vous faites appel.

Memoisation : memo, useMemo et useCallback

La memoisation est la technique d'optimisation React la plus discutee et la plus mal appliquee. L'idee centrale est simple : si une fonction produit la meme sortie pour les memes entrees, mettez en cache le resultat et sautez le calcul lors des appels suivants. React fournit trois outils de memoisation, chacun servant un objectif different.

React.memo enveloppe un composant et empeche les re-rendus quand ses props n'ont pas change (en utilisant la comparaison superficielle). Il est plus efficace pour les composants feuilles qui recoivent frequemment les memes props - pensez aux elements de liste, aux elements de graphique ou a tout composant pur dont la sortie de rendu ne depend que de ses props.

import { memo } from "react";

const ExpenseRow = memo(function ExpenseRow({ label, amount }: {
  label: string;
  amount: number;
}) {
  return (
    <tr>
      <td>{label}</td>
      <td className={amount < 0 ? "text-red-500" : "text-green-500"}>
        ${amount.toFixed(2)}
      </td>
    </tr>
  );
});

// Maintenant ExpenseRow ne re-rend que quand label ou amount change.
// Sans memo, il re-rend chaque fois que le parent re-rend.

useMemo met en cache le resultat d'un calcul entre les rendus. Il est utile quand un composant effectue un calcul couteux a chaque rendu - filtrer un grand tableau, formater des donnees ou executer une transformation complexe. Sans useMemo, ce travail se repete a chaque re-rendu meme si les entrees n'ont pas change.

function Dashboard({ transactions, filter }: Props) {
  // Sans useMemo : ceci s'execute a chaque rendu
  const visibleTransactions = useMemo(
    () => transactions
      .filter(t => t.date >= filter.start && t.date <= filter.end)
      .sort((a, b) => b.amount - a.amount),
    [transactions, filter]
  );

  // Sans useMemo, filtrer et trier 10 000 elements
  // se produit a chaque frappe dans n'importe quelle entree de la page.
  return <TransactionList items={visibleTransactions} />;
}

useCallback est identique a useMemo mais pour les fonctions. Il retourne une version memoisee d'une fonction de rappel qui ne change que quand ses dependances changent. Cela importe car les fonctions definies dans le corps du rendu sont recreees a chaque rendu, ce qui casse la comparaison superficielle que memo utilise.

Voici la regle critique que la plupart des guides trompent : n'enveloppez pas tout dans useMemo et useCallback. Chaque appel de memoisation a un cout - stocker le tableau de dependances, le comparer a chaque rendu et allouer la memoire de fermeture. Appliquer la memoisation a des operations peu couteuses rend votre application plus lente, pas plus rapide. L'heuristique est simple : ne memoisez que si vous avez mesure un probleme, ou si le calcul est manifestement couteux (transformations de donnees complexes, operations recursives, rendus de grandes listes).

La memoisation la plus couteuse est celle qui n'aurait pas du etre ecrite. Profilez d'abord. Enveloppez ensuite. Chaque abstraction a un cout, et la memoisation est une abstraction du cycle de rendu.

Un anti-pattern courant consiste a envelopper systematiquement tout avec memo de maniere defensive, en esperant prevenir les problemes de performance. Cela ne fonctionne pas. Cela ajoute une surcharge a chaque comparaison de rendu, gonfle le bundle avec du code supplementaire et rend la codebase plus difficile a raisonner. Ajoutez la memoisation comme une optimisation ciblee apres que le profilage a confirme qu'elle aide, pas comme un modele par defaut.

Division de code avec React.lazy et Suspense

La taille du bundle est la dimension de performance la plus negligee dans les applications React. Les developpeurs obsedes par l'optimisation du rendu tout en expediant 500 Ko de JavaScript que chaque utilisateur doit telecharger, analyser et executer avant de voir quoi que ce soit. La division de code resout ce probleme en divisant votre bundle en morceaux qui se chargent a la demande.

React.lazy vous permet de rendre un composant importe dynamiquement comme un composant regulier. Combine avec Suspense, vous pouvez afficher une interface de repli pendant que le morceau se charge. Le gain de performance est double : le bundle initial est plus petit, donc la page se charge plus vite, et les utilisateurs ne paient que pour le code qu'ils utilisent reellement.

import { lazy, Suspense } from "react";

const AnalyticsDashboard = lazy(
  () => import("./AnalyticsDashboard")
);
const DataExportPanel = lazy(
  () => import("./DataExportPanel")
);

function App() {
  const [showAnalytics, setShowAnalytics] = useState(false);

  return (
    <div>
      <button onClick={() => setShowAnalytics(true)}>
        Voir les analytics
      </button>
      <Suspense fallback={<div>Chargement...</div>}>
        {showAnalytics && <AnalyticsDashboard />}
      </Suspense>
    </div>
  );
}

// AnalyticsDashboard et ses dependances ne sont charges que
// quand l'utilisateur clique sur le bouton, pas au chargement initial.

Les meilleurs candidats pour le chargement differe sont les composants au niveau des routes, les bibliotheques de visualisation lourdes (graphiques, diagrammes, cartes), les editeurs de texte riches et tout composant qui se trouve en dessous de la ligne de flottaison ou qui est declenche par une interaction utilisateur. Une bonne regle empirique : si un composant ajoute plus de 20 Ko au bundle et n'est pas visible au rendu initial, il devrait probablement etre charge differement.

Le comportement ameliore de Suspense dans React 19 rend la division de code encore plus pratique. La recuperation de donnees a l'interieur des limites Suspense est desormais entierement integree au cycle de vie du rendu, eliminant le probleme de cascade ou vous deviez charger le morceau d'abord, puis recuperer ses donnees. Combine avec le nouveau hook use() pour lire les promesses, les frontieres entre les etats de chargement deviennent plus propres et plus composables.

Defilement virtuel et optimisation des listes

Le rendu de grandes listes est le probleme de performance le plus courant dans les applications React reelles. Une liste de 10 000 elements fonctionne parfaitement en developpement et s'effondre en production car React doit creer et reconcilier 10 000 noeuds DOM virtuels a chaque rendu. Le moteur de mise en page du navigateur doit ensuite calculer les positions pour 10 000 noeuds DOM. Le resultat est une interface gelée et un utilisateur frustre.

Le defilement virtuel resout ce probleme en ne rendant que les elements visibles dans la fenetre d'affichage, plus un petit tampon au-dessus et en-dessous. Quand l'utilisateur fait defiler, les elements hors de la fenetre sont demontees et de nouveaux elements sont montes. Le DOM reste petit - generalement 20 a 30 noeuds quelle que soit la taille de la liste - et le cout de reconciliation de React reste constant.

import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: "absolute",
              top: 0,
              transform: `translateY(${virtualItem.start}px)`,
              height: virtualItem.size,
              width: "100%",
            }}
          >
            <ItemRenderer item={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

// Seulement ~25-30 lignes sont rendues quelle que soit la taille de la liste.
// 100 000 elements ? Meme cout de rendu.

Au-dela du defilement virtuel, la performance des listes depend de cles stables. Utiliser l'index du tableau comme cle amene React a mal identifier les elements modifies, ce qui entraine des demontages et remontages inutiles de noeuds DOM. Utilisez un identifiant unique de vos donnees - l'ID de l'element - comme cle. Cela permet a React de reutiliser les noeuds DOM quand les elements sont reordonnes, inseres ou supprimes, ce qui est significativement moins couteux que de les detruire et les recreer.

  • Utilisez le defilement virtuel pour toute liste susceptible de depasser 200 a 500 elements. Le seuil depend de la complexite des elements, mais 500 lignes de complexite moderee est le point ou vous commencez a remarquer des saccades.
  • Utilisez toujours des cles stables et uniques derivees de vos donnees (IDs d'elements). Les cles indexes sont un dernier recours pour les listes statiques qui ne changent jamais.
  • Gardez les composants d'elements de liste legers. Chaque element doit etre un simple composant de presentation avec un minimum d'appels de hooks. Les calculs couteux dans les elements de liste se multiplient sur chaque ligne rendue.
  • Envisagez le fenetrage ou la pagination pour les listes qui n'ont pas besoin d'afficher tous les elements a la fois. Parfois, la meilleure optimisation est de ne rien rendre du tout.

Profilage, analyse de bundle et modeles React 19+

L'outil de performance le plus important n'est pas une bibliotheque ou un hook. C'est le profileur React DevTools. Avant d'optimiser quoi que ce soit, enregistrez une session de profilage en reproduisant le comportement lent. Le profileur vous montre exactement quels composants ont ete rendus, pourquoi (changement de prop, changement d'etat, changement de contexte, re-rendu du parent) et combien de temps chaque rendu a pris. Sans ces donnees, vous devinez.

L'analyse de bundle est le deuxieme outil essentiel. Une application React rapide qui expedie 800 Ko de JavaScript est toujours lente car le navigateur doit la telecharger et l'analyser. Des outils comme vite-bundle-visualizer, statoscope ou source-map-explorer generent une representation visuelle de votre bundle, montrant quels paquets contribuent le plus d'octets. Les resultats sont souvent surprenants - une seule grosse dependance (moment.js, une bibliotheque de graphiques, un jeu d'icones) peut representer la moitie de votre bundle.

React 19 introduit des modeles qui changent le paysage de l'optimisation des performances. Les composants serveur (Server Components) s'executent sur le serveur et n'envoient que le HTML rendu au client, eliminant le JavaScript cote client pour les composants qui n'ont pas besoin d'interactivite. Ce n'est pas une optimisation de niche - tout composant qui recupere des donnees et les affiche sans etat cote client ni gestionnaires d'evenements devrait etre un composant serveur. Le JavaScript qui aurait ete envoye au client est zero.

La distinction entre composants serveur et composants client cree un nouveau modele mental pour la performance. Avant de recourir a memo, demandez : ce composant a-t-il besoin de s'executer sur le client ? Si la reponse est non, vous avez elimine tout cout de re-rendu, tout cout d'hydration et tout octet de JavaScript que ce composant aurait contribue au bundle. Les composants serveur ne remplacent pas les outils d'optimisation cote client de React. Ce sont une strategie de plus haut niveau qui rend les optimisations cote client inutiles pour une grande partie de votre arbre de composants.

L'optimisation des images est un autre domaine ou le framework gere ce que les developpeurs faisaient manuellement. Les composants Image de Next.js servent automatiquement des images responsives a la bonne taille et au bon format, chargent differeement les images en dessous de la ligne de flottaison et empechent le decalage de mise en page en reservant l'espace avant le chargement de l'image. Si vous utilisez encore de simples balises img avec des JPEG volumineux, ce seul changement produit souvent une amelioration de performance plus importante que n'importe quelle strategie de memoisation.

  • Executez le profileur React DevTools sur l'interaction la plus lente de votre application avant d'apporter des modifications. Sauvegardez l'enregistrement comme reference.
  • Analysez votre bundle avec vite-bundle-visualizer ou source-map-explorer. Recherchez les dependances volumineuses qui peuvent etre remplacees par des alternatives plus legeres.
  • Convertissez les composants de recuperation de donnees en composants serveur si vous utilisez React 19 avec un framework serveur. Cela elimine leur cout cote client entier.
  • Remplacez les balises img par le composant Image du framework (next/image, etc.) pour des images responsives automatiques et un chargement differe.
  • Utilisez l'onglet performance de Chrome DevTools pour mesurer le delai d'interaction, les decalages de mise en page et les taches longues - pas seulement le temps de rendu React.

Tout assembler

L'optimisation des performances React suit une hierarchie claire. Au sommet se trouve l'architecture : les composants serveur qui s'executent sur le serveur, la division de code qui maintient les bundles petits et le defilement virtuel qui limite la taille du DOM. Au milieu se trouve la memoisation ciblee : memo pour les composants feuilles qui re-rendent inutilement, useMemo pour les calculs couteux et useCallback pour les references de callbacks stables. En bas - et c'est la que la plupart des gens commencent - se trouve la micro-optimisation : fonctions inline, references d'objets et enveloppement premature qui ajoutent de la complexite sans benefice mesurable.

Travaillez du haut vers le bas. Profilez d'abord, puis verifiez votre architecture, puis appliquez une memoisation ciblee. Ne commencez pas par les micro-optimisations. La difference entre une application React bien optimisee et une application mal optimisee n'est presque jamais de savoir si vous avez utilise memo aux bons endroits. C'est de savoir si votre architecture permet a React de faire son travail efficacement - petits arbres de composants, petits bundles, petites tailles de DOM et flux de donnees clairs.

Les techniques de ce guide sont completes mais le principe est simple : rendez le comportement par defaut de React rapide en lui donnant moins de travail a faire. Rendrez moins de composants. Expédiez moins de JavaScript. Calculez moins de valeurs. Moins vous demandez a React, plus il sera rapide - et moins vous aurez besoin de penser a la performance.