Accueil / Articles

Ce que le compilateur React optimise et ce qu’il vous laisse à gérer

Comprenez quels aspects de la performance le compilateur React automatise, pourquoi les API lentes et les fichiers volumineux restent de votre responsabilité, ainsi que la manière de l’intégrer en toute sécurité dans une base de code React existante.

Éditeur : REACTAPP.TOP6 min de lecture1421 mots

Sources: React Compiler Won’t Fix a Slow React App

Le compilateur React élimine une grande partie de la mémorisation manuelle, ce qui pousse à le considérer comme une solution aux applications lentes. Ce n’est pas le cas : il optimise uniquement les tâches de rendu contrôlées par React, et rien d’autre. Vous verrez ci-dessous où se situe cette limite, quels ralentissements nécessitent encore l’intervention d’un ingénieur, et comment activer le compilateur dans une base de code existante sans provoquer de régressions.

De la liste de contrôle de la mémorisation à la mémorisation automatique

Pendant longtemps, les conseils pour améliorer les performances de React se résumaient à une courte liste d’outils :

useMemo()
useCallback()
React.memo()

Un composant était re-renderisé trop souvent, alors on l’a enveloppé. Un calcul semblait coûteux, donc on l’a mis en cache. Une fonction de rappel était appelée depuis un enfant, alors on l’a stabilisée. Chaque solution reposait sur une mémorisation manuelle, avec un tableau de dépendances à gérer.

Le compilateur React modifie cette valeur par défaut. La version 1.0 est considérée comme stable, et la recommandation de l’équipe React pour le nouveau code consiste à laisser le compilateur gérer la mémorisation au lieu d’utiliser manuellement useMemo et useCallback. Consultez la documentation actuelle de React pour obtenir les dernières conseils d’adoption.

Le point clé se résume en une phrase : le compilateur peut rendre le travail propre à React moins coûteux, mais il n’a aucune influence sur les opérations qui se déroulent en dehors de React.

Que fait réellement le compilateur

Lors de la compilation, le compilateur analyse les composants et les hooks et insère des mécanismes de mémorisation là où son analyse montre qu’ils sont utiles. Prenons un composant qui filtre une liste de produits avant de la rendre :

function ProductList({ products }) {
  const visible = products.filter(
    product => product.inStock
  );

Le tableau filtré est ensuite transmis directement à un composant enfant :

  return <List items={visible} />;
}

Auparavant, un développeur soucieux des performances stockait souvent explicitement le filtre en mémoire afin que List reçoive toujours la même référence d’array tant que products ne changeait pas :

const visible = useMemo(
  () => products.filter(
    product => product.inStock
  ),
  [products]
);

Avec le compilateur activé, on omet généralement ce code. Il détermine lui-même les dépendances, peut mémoriser de manière plus précise que les hooks écrits manuellement, et ne dépend pas de vous pour une liste correcte des dépendances. React appelle cela la mémorisation automatique pour les composants et les hooks.

Prêtez cependant attention au contexte dans cette description : chaque élément de la liste est une construction React.

Un modèle mental : un optimiseur de rendu

Il est utile d’imaginer le compilateur comme un outil qui s’applique aux deux types de tâches React et les simplifie tous les deux :

React Compiler
                      │
          ┌───────────┴───────────┐
          ▼                       ▼
    Component work          Hook calculations
          │                       │
          ▼                       ▼
     memoization             memoization
          │                       │
          └───────────┬───────────┘
                      ▼
              fewer unnecessary
                 React updates

Un nombre réduit de re-renderisations inutiles peut être un véritable avantage. Cependant, la partie importante du diagramme réside dans ce qu’il omet.

Les backends lents sont hors de sa portée

Considérez un composant asynchrone qui attend une réponse d’une API avant de rendre un tableau de bord :

async function Dashboard() {
  const data = await fetch(
    "https://api.example.com/dashboard"
  );

Lorsque les données arrivent, elles sont transmises à l’interface utilisateur :

  return <DashboardUI data={data} />;
}

Si cet endpoint met 800 ms pour répondre, le compilateur ne parviendra pas à réduire ce temps à 50 ms. Il peut accélérer la conversion des données en interface utilisateur au sein de React, mais pas faire réagir votre backend plus rapidement. En termes de chronologie :

Browser
   │
   ▼
React
   │
   ▼
API request ──────────► 800ms
   │
   ▼
Response
   │
   ▼
React render

La mémorisation concerne presque entièrement l’étape finale. Si la plupart du temps est consacrée à l’attente réseau, vous optimisez le mauvais niveau ; les solutions consistent en des requêtes plus rapides, du cache, des réponses plus petites ou un chargement parallèle. Pour séparer le temps réseau du temps côté client, consultez Diagnostiquer les problèmes de performance de React au-delà du temps de réponse API.

JavaScript que vous n’auriez pas dû inclure

La même limite s’applique à la taille du bundle. Imaginez une page d’accueil qui télécharge une bibliothèque de graphiques lourde, un éditeur de texte enrichi et un SDK d’analyse avant même que quiconque n’ouvre les écrans qui les utilisent :

Initial page
     │
     ├── React
     ├── UI
     ├── Chart library
     ├── Editor
     └── Analytics SDK

Le compilateur ne supprime aucune dépendance ; chaque octet est toujours téléchargé, analysé et exécuté. Ce qui aide, c’est le splitage du code, par exemple en chargeant l’écran d’analyse sur demande :

const Analytics = lazy(
  () => import("./Analytics")
);

Désormais, le code n’est chargé que lorsque la fonctionnalité est réellement affichée, et une balise Suspense environnante affiche un contenu de remplacement pendant son chargement. Il s’agit d’une décision liée à l’architecture en bundles, et aucune méthode de mémorisation ne peut la remplacer.

Où les attentes échouent

Histoire courante : une application semble lente, quelqu’un active le compilateur, et l’équipe s’attend à ce résultat :

Slow app
   │
   ▼
React Compiler
   │
   ▼
Fast app

C’est trop simpliste. La lenteur a plusieurs causes, et le compilateur ne traite qu’un seul aspect parmi eux :

SLOW APP
                       │
       ┌───────────────┼───────────────┐
       ▼               ▼               ▼
    React work      Network          Browser
       │               │               │
       ▼               ▼               ▼
   Compiler        API/cache       JS/rendering
       │               │               │
       ▼               ▼               ▼
   Optimized       Engineer         Engineer

Le compilateur n’est qu’un outil parmi ceux disponibles pour améliorer les performances, et non l’ensemble de ces outils. La latence réseau ainsi que les coûts liés au navigateur, tels que l’évaluation des scripts et le calcul du rendu, nécessitent toujours une intervention humaine.

Faut-il supprimer les appels existants à useMemo ?

Non automatiquement. Dans le nouveau code, la configuration par défaut officielle consiste à permettre au compilateur de mémoriser les valeurs. Pour le code existant, React recommande de laisser en place la mémorisation actuelle, ou de l’enlever uniquement après des tests approfondis. La raison en est liée au comportement du code et non seulement à ses performances : une valeur mémorisée utilisée comme dépendance d’un Effect détermine quand cet Effect sera exécuté à nouveau, donc modifier la manière dont elle est mémorisée peut influencer le moment où les effets secondaires se produisent.

Il convient donc de résister à l’envie d’effectuer un nettoyage à l’échelle de toute la base de code comme ceci :

useMemo      → DELETE
useCallback  → DELETE
React.memo   → DELETE

Une approche plus sûre consiste à procéder progressivement, en effectuant des mesures à chaque étape :

Existing app
     │
     ▼
Enable compiler
     │
     ▼
Run tests
     │
     ▼
Profile
     │
     ▼
Remove unnecessary
manual memoization
carefully

Le compilateur a pour but de rendre la mémorisation manuelle moins nécessaire à l’avenir, et non de déclarer toutes les optimisations existantes incorrectes du jour au lendemain.

Le compilateur en tant que vérificateur de qualité du code

Un avantage qui reçoit moins d’attention : le compilateur intègre des étapes de validation basées sur les règles de React, et son intégration avec lint peut signaler des schémas tels que ceux-ci :

set-state-in-render
set-state-in-effect
unsafe ref access

React indique que ces diagnostics peuvent révéler des bugs déjà présents dans une application. Comme le compilateur ignore le code qui contredit ses hypothèses, les avertissements montrent également quels composants ne sont pas optimisés, ce qui en fait partie du cycle de feedback quotidien plutôt que simplement un plugin de performance.

Profillez d’abord, puis choisissez la correction

Lorsqu’une interface semble lente, mesurez avant d’utiliser un outil quelconque, et laissez les résultats de la mesure guider votre choix :

SLOW UI
                 │
                 ▼
               PROFILE
                 │
        ┌────────┼────────┐
        ▼        ▼        ▼
      React    Network   Browser
       work      │        │
        │        │        │
        ▼        ▼        ▼
    Compiler   API/cache  JS/render
        │
        ▼
   Targeted fix

Ensuite, adaptez la correction en fonction de ce que montre le profileur :

  • Réaffichages inutiles de React : c’est là que le React Compiler peut être utile.
  • API lente : améliorez l’endpoint ou la stratégie de récupération des données.
  • Un paquet trop volumineux : envoyez moins de JavaScript en le divisant et en réduisant les dépendances.
  • Mises à jour DOM coûteuses : repensez la manière dont vous affichez le contenu, par exemple en virtualisant de longues listes.
  • Un script tiers lourd : supprimez-le, ajoutez son exécution à un moment ultérieur ou isolez-le.
  • Points clés

    Au modèle ancien, il fallait indiquer à React ce qu’il fallait mémoriser ; le nouveau modèle vous permet d’écrire du code React correct et de confier une grande partie du travail d’affichage au compilateur. Cela valorise une compétence essentielle : savoir où passe réellement le temps.

    • Le compilateur optimise les tâches qu’il comprend, c’est-à-dire le rendu React et les calculs liés aux hooks.
    • Il ne peut pas réduire la taille d’une réponse API trop volumineuse, accélérer une requête de base de données, supprimer un script tiers ou compresser une image de 5 MB.
    • Il ne peut pas sauver une architecture qui crée inutilement du travail partout.
    • Dans le code existant, activez-le, testez-le, profilez-le, et n’éliminez la mémorisation manuelle qu’ensuite.
    • Tenez ses diagnostics pour des rapports d’erreurs gratuits concernant les violations des règles de React.

    Ne vous demandez pas si le compilateur est activé, mais où l’application passe son temps ; c’est justement pour cela que le compilateur existe, afin de gérer la part liée à React.

    Lectures complémentaires