Depuis la création de React, la complexité de nos applications a énormément augmenté. Le nombre d'outils, de bibliothèques, de configurations et de dépendances nécessaires pour créer une nouvelle application a augmenté de façon exponentielle, et tout cela pèse lourdement sur la productivité de nos applications.

Et, oui, React dans son ensemble est fantastique, il emploie un certain nombre de techniques pour minimiser la quantité d'opérations DOM coûteuses nécessaires pour mettre à jour l'interface utilisateur de nos applications. Mais selon une étude d'Akamai, un retard d'une seconde dans le temps de chargement peut entraîner une diminution de 7 % des conversions, ce qui fait qu'il est essentiel pour les développeurs de créer des apps aux performances optimales.

Dans cet article, nous allons passer en revue les moyens d'améliorer les performances d'une application React, y compris certaines techniques de pré-optimisation, particulièrement utilise lorsqu'on est un développeur react freelance

Profiler votre application React et rechercher les goulets d'étranglement

Je pense qu'optimiser sans mesurer est presque toujours prématuré, c'est pourquoi je recommanderais de faire un benchmarking et de mesurer les performances en premier. Vous pouvez utiliser les outils Chrome Timeline et React Dev pour profiler et visualiser les composants. Cela vous permet de voir quels composants sont démontés, montés et mis à jour, ainsi que le temps qu'ils prennent les uns par rapport aux autres. Cela vous aidera à commencer votre voyage d'optimisation des performances.

Comprendre comment React met à jour son interface utilisateur

Lorsque nous créons un nouveau composant, React crée un DOM virtuel pour son arbre d'éléments dans le composant. Lorsque l'état du composant change, React recrée l'arbre DOM virtuel et compare le résultat au rendu précédent. Ce n'est qu'ensuite qu'il met à jour l'élément modifié dans le DOM, c'est ce qu'on appelle le diffing.

Parce que le DOM réel est coûteux à manipuler, React utilise le concept de DOM virtuel pour réduire le coût de performance du nouveau rendu d'une page Web. Cela est bénéfique car cela réduit le temps de rendu de l'interface utilisateur. Cependant, s'il n'est pas correctement géré, ce concept peut ralentir une application complexe.

Ce que nous pouvons en conclure, c'est que la modification de l'état d'un composant React entraîne un nouveau rendu. De même, lorsque l'état est transmis en tant que prop à un composant enfant, le rendu de ce dernier est modifié, et ainsi de suite, ce qui est normal puisque React doit mettre à jour l'interface utilisateur.

Le problème survient lorsque les composants enfants ne sont pas affectés par le changement d'état. En d'autres termes, ils n'ont pas reçu de prop de la part du composant parent. Néanmoins, React rend à nouveau ces composants enfants. Ainsi, tant que le composant parent est rendu, tous ses composants enfants sont rendus, qu'une prop leur soit transmise ou non, c'est le comportement par défaut de React. Donc, fondamentalement, lorsque l'état du composant App change, le composant enfant se réactualise, même s'il n'est pas directement affecté par le changement d'état.

Ce nouveau rendu ne devrait pas causer de problèmes de performance dans la plupart des cas, et nous ne devrions pas remarquer de décalage dans notre application. En revanche, si le composant effectue un calcul coûteux et que nous remarquons des problèmes de performance, nous devons optimiser notre application React.

Mesurer les performances avec ReactDevTools

Nous pouvons utiliser le profileur dans les React DevTools pour mesurer les performances de nos applications. Chaque fois que notre application effectue un rendu, nous pouvons collecter des données de performance.

Le profileur consigne le temps de rendu d'un composant, la raison de ce rendu et d'autres informations. Nous pouvons ensuite examiner le composant concerné et effectuer les optimisations nécessaires. Pour utiliser le profileur, vous devez d'abord installer React DevTools pour votre navigateur préféré.

Le profilage de l'application révèle le comportement suivant :

capture d'écran des ReactDevTools

Lorsque le champ de texte de saisie se met à jour, nous recevons les détails des composants rendus, le profileur DevTools met en évidence chaque composant rendu. Nous pouvons voir combien de temps il a fallu pour rendre les composants et pourquoi le composant App est rendu dans le graphique de flamme ci-dessous.

capture d'écran des react dev tools

De même, l'image ci-dessous montre le rendu du composant enfant suite au rendu du composant parent.

catpure d'écran react devtools

Si une opération dans un composant enfant prend du temps à calculer, cela peut avoir un effet sur les performances de l'application React. Maintenant que vous avez compris comment profiler votre application React et rechercher les composants bloquants et les goulots d'étranglement dans vos composants, revenons à nos méthodes d'optimisation.

Techniques d'optimisation des performances de React

Quelques points importants pour optimiser votre application.

1. Maintenir la composante locale de l'État si nécessaire

Les mises à jour d'état sont fréquentes dans les applications complexes et de grande taille. Certaines sont asynchrones, comme les appels HTTP en arrière-plan, tandis que d'autres se produisent par l'utilisateur ou par vous, et les composants sont fréquemment rendus plusieurs fois avant toute interaction avec l'utilisateur. Il est de la responsabilité des développeurs de détecter les rendus gaspillés et d'éviter les rapprochements lorsqu'aucun rendu n'est nécessaire. Ceci est particulièrement gênant si les rendus gaspillés se produisent dans les composants parents et que React redessine tous les composants dans l'arbre et gaspille des cycles CPU.

Ainsi, pour s'assurer que le nouveau rendu d'un composant ne se produit que lorsque cela est nécessaire, nous pouvons extraire le code qui est responsable de l'état du composant et le rendre local à cette partie du code. Cela garantit que seul le composant concerné par l'état est rendu.

Mais parfois, nous ne pouvons pas éviter d'avoir un état dans un composant global tout en le transmettant aux composants enfants en tant qu'accessoire. Dans ce cas, nous allons apprendre comment éviter de rendre à nouveau les composants enfants non affectés.

2. Mémorisation des composants React pour éviter les rerendus inutiles

La [object Promise] est une technique utilisée pour accélérer votre code en mettant en cache les résultats des appels de fonctions coûteux et en renvoyant le résultat en cache lorsque les mêmes entrées sont à nouveau utilisées. Une fonction mémorisée est généralement plus rapide car si elle est appelée avec les mêmes valeurs que la précédente, elle ira chercher le résultat dans le cache au lieu d'exécuter la logique de la fonction.

Considérons le composant React simple et sans état UserDetails suivant. Lorsque les accessoires changent, ce composant sans statut sera rendu à nouveau. Si l'attribut du composant UserDetails est moins susceptible de changer, alors c'est un bon candidat pour utiliser la version memoize du composant.

import memo from 'memo';

const UserDetails = ({user, onEdit}) =>{
    const {title, full_name, profile_img} = user;

    return (
        <div className="user-detail-wrapper">
            <img src={profile_img} />
            <h4>{full_name}</h4>
            <p>{title}</p>
        </div>
    )
}

export default memo(UserDetails,{
    isReact: true
});

En utilisant React.memo()

React.memoest un composant d'ordre supérieur qui est utilisé pour envelopper un composant purement fonctionnel afin d'éviter un nouveau rendu si les props reçus dans ce composant ne changent jamais :

import React, { useState } from "react";

// ...

const ChildComponent = React.memo(function ChildComponent({ count }) {
  console.log("child component is rendering");
  return (
    <div>
      <h2>This is a child component.</h2>
      <h4>Count: {count}</h4>
    </div>
  );
});

Lorsque nous transmettons des valeurs primitives, comme un nombre dans notre exemple, React.memo() fonctionne bien. En revanche, les valeurs non primitives, comme les tableaux et les fonctions, renvoient toujours false entre les réexamens car elles pointent vers des espaces mémoire différents. Le composant mémorisé est toujours rendu lorsque nous transmettons un objet, un tableau ou une fonction en tant qu'accessoire. Pour empêcher la fonction de toujours se redéfinir, nous utiliserons un Hook useCallback qui renvoie une version mémorisée du callback entre les rendus.

En utilisant le hook useCallback

Avec le hook useCallback, la fonction incrementCount ne se redéfinit que lorsque le tableau de dépendance du compte change :

const incrementCount = React.useCallback(() => setCount(count + 1), [count]);

En utilisant le hook useMemo()

UseMemo est un hook qui mémorise la valeur de retour d'une fonction. Il accepte deux arguments : l'un est une fonction qui retourne la valeur à mémoriser, et l'autre est un tableau de dépendances. Le hook vérifie les changements dans les valeurs passées dans ce tableau de dépendances. Si un changement de dépendance est détecté, la fonction passée en premier argument est exécutée une fois de plus. Si aucun changement ne se produit, le hook useMemo renvoie la valeur mémorisée stockée lors d'une invocation précédente.

Voyons comment appliquer le crochet useMemo pour améliorer les performances d'une application React. Jetez un œil au code suivant que nous avons intentionnellement retardé pour qu'il soit très lent.

const expensiveFunction = (count) => {
  // artificial delay (expensive computation)
  for (let i = 0; i < 1000000000; i++) {}
  return count * 3;
};

export default function App() {
  // ...
  const myCount = expensiveFunction(count);
  return (
    <div>
      {/* ... */}
      <h3>Count x 3: {myCount}</h3>
      <hr />
      <ChildComponent count={count} onClick={incrementCount} />
    </div>
  );
}

const ChildComponent = React.memo(function ChildComponent({ count, onClick }) {
  // ...
});

Et l'initialiser dans notre code :

const myCount = React.useMemo(() => {
  return expensiveFunction(count);
}, [count]);

La expensiveFunction ne doit être appelée que lorsque l'on clique sur le bouton de comptage, et non lorsque l'on tape dans le champ de saisie. Nous pouvons mémoriser la valeur renvoyée par la expensiveFunction à l'aide du hook useMemo afin que la fonction ne soit recalculée que lorsque cela est nécessaire, c'est-à-dire lorsque le bouton de comptage est cliqué.

3. Fractionnement du code dans React à l'aide de la fonction import() dynamique

La taille du bundle JavaScript est l'une des principales causes de la lenteur du chargement des applications client riches. Par défaut, webpack met tout dans un seul bundle. Ainsi, le navigateur télécharge toujours l'ensemble du code, y compris les routes rarement utilisées, les popups de configuration et d'autres trucs au cas où. Les bibliothèques lourdes seront également présentes, même si la seule chose dont vous avez besoin est la page "about us".

plusieurs papiers blancs séparés

Le fractionnement du code est le moyen le plus efficace de lutter contre ce problème. Vous devrez charger paresseusement et de manière asynchrone certaines parties de votre code et ne les utiliser que lorsqu'elles sont nécessaires. Suspense et React.lazy peuvent vous aider à le faire, et react-router prend également en charge le lazyload des routes.

Lorsqu'il s'agit d'applications purement côté client, le chargement paresseux est assez simple, mais les choses se compliquent lorsqu'il s'agit de rendu côté serveur. Comme vous n'avez qu'une seule chance de rendre la page, le code asynchrone ne fonctionnera pas. Tout ce qui est chargé paresseusement sur le client doit être prêt à être rendu sur le serveur à l'avance. React.lazy et Suspense ne sont pas disponibles pour le rendu côté serveur.

Le fractionnement du code React nous permet de diviser un gros fichier bundle en plusieurs morceaux à l'aide de la fonction import() dynamique, puis de charger ces morceaux à la demande à l'aide de React.lazy. Cette approche améliore considérablement les performances des pages d'une application React complexe. Pour mettre en œuvre le fractionnement du code, nous modifions un import React standard comme suit :

const Home = React.lazy(() => import("./components/Home"));
const About = React.lazy(() => import("./components/About"));

Cette syntaxe indique à React de charger dynamiquement chaque composant. Par conséquent, lorsqu'un utilisateur clique sur un lien vers la page d'accueil, React ne télécharge que le fichier de la page demandée plutôt que de charger un gros fichier groupé pour l'ensemble de l'application.

Après l'importation, nous devons effectuer le rendu des composants paresseux au sein d'un composant Suspense comme suit :

<React.Suspense fallback={<p>Loading page...</p>}>
  <Route path="/" exact>
    <Home />
  </Route>
  <Route path="/about">
    <About />
  </Route>
</React.Suspense>

Le Suspense nous permet d'afficher un texte ou un indicateur de chargement comme solution de repli pendant que React attend de rendre le composant paresseux dans l'interface utilisateur.

Vous pouvez en savoir plus à ce sujet ici.

4. Virtualiser les longues listes

La virtualisation des listes, également connue sous le nom de fenêtrage, est une méthode permettant d'améliorer les performances lors du rendu d'une longue liste de données. Cette technique ne rend qu'un sous-ensemble de lignes à la fois et peut réduire de manière significative le temps nécessaire pour rendre à nouveau les composants ainsi que le nombre de nœuds DOM créés.

exemple de liste

Les bibliothèques React populaires comprennent react-window et react-virtualized, qui fournissent des composants réutilisables pour l'affichage de listes, de grilles et de données tabulaires.

Références et articles connexes : Virtualiser de longues listes, react-window, react-virtualized

5. Lazyload des images dans React

Pour améliorer le temps de chargement des pages d'une application qui contient beaucoup d'images, nous pouvons éviter de les rendre toutes en même temps. Nous pouvons utiliser le chargement paresseux pour retarder le rendu des images dans le DOM jusqu'à ce qu'elles soient sur le point d'apparaître dans la fenêtre d'affichage.

Le lazyload des images, comme le fenêtrage, empêche la création de nœuds DOM inutiles, améliorant ainsi les performances de notre application React.

Les bibliothèques de chargement lazyload populaires pour les projets React comprennent react-lazyload et react-lazy-load-image-component. Vous pouvez les utiliser comme ceci :

import React from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';

const MyImage = ({ image }) => (
  <div>
    <LazyLoadImage
      alt={image.alt}
      height={image.height}
      src={image.src} // use normal <img> attributes as props
      width={image.width} />
    <span>{image.caption}</span>
  </div>
);

export default MyImage;

6. Fichiers Chunk multiples

Chaque application commence avec quelques composants. Vous commencez par ajouter de nouvelles fonctionnalités et dépendances, et avant de vous en rendre compte, vous avez un énorme fichier de production.

Vous pouvez envisager de séparer les fichiers en utilisant le plugin "SplitChunksPlugin" pour webpack. En divisant vos fichiers, votre navigateur les met moins souvent en cache et télécharge les ressources en parallèle pour réduire le temps d'attente au chargement.

Ce plugin s'attaque notamment au problème de la duplication du code, qui peut entraîner le chargement d'un contenu redondant sur le réseau. Le SplitChunkPlugin peut déterminer, en fonction d'une certaine configuration, quels modules sont trop coûteux pour être dupliqués et les placer dans des morceaux séparés, de sorte que l'effort important de chargement de gros modules ne soit fait qu'une seule fois.

Comment optimiser une application frontend de manière globale ?

Il n'y a pas que React, voici une liste de conseil pour optimiser votre application.

1 - Utiliser un CDN

Cela peut sembler évident, mais vous devriez utiliser un réseau de diffusion de contenu (CDN). Il sert vos ressources à partir d'un emplacement plus proche de l'utilisateur, ce qui se traduit par une bande passante plus large et des latences plus faibles. Ils sont conçus pour la vitesse et sont généralement peu coûteux.

une image représentant CDN

Les CDN peuvent également servir de proxy et mettre en cache vos appels API si nécessaire, et ils respectent généralement les en-têtes Cache-Control, ce qui vous permet de définir des délais d'expiration différents pour chaque ressource. Les CDN modernes peuvent même être configurés pour ignorer des paramètres de requête spécifiques ou pour mettre en cache les cookies. Certains d'entre eux peuvent ajouter des en-têtes personnalisés à vos requêtes, comme l'adresse IP d'origine ou la géolocalisation IP.

2 - Utilisez des animations CSS plutôt que des animations JS

Les animations sont créées pour offrir une expérience utilisateur fluide et agréable. Il existe de nombreuses approches pour mettre en œuvre des animations Web. En général, nous pouvons créer des animations de trois façons : les transitions CSS, les animations CSS et le JavaScript.

Les transitions CSS permettent de créer facilement des animations entre le style actuel et un état CSS final, tel qu'un état de bouton de repos et un état de survol. Même si un élément se trouve au milieu d'une transition, la nouvelle transition commence avec le style actuel plutôt qu'avec l'état CSS final. Pour plus d'informations, voir Utilisation des transitions CSS.

Les animations CSS, quant à elles, permettent aux développeurs de créer des animations entre un ensemble de valeurs de propriétés initiales et un ensemble de valeurs de propriétés finales plutôt qu'entre deux états. Les animations CSS se composent de deux parties : un style qui décrit l'animation CSS et un ensemble d'images clés qui indiquent les états de début et de fin du style de l'animation, ainsi que les éventuels points intermédiaires.

3 - Ne plus utiliser CSS-in-JS

En gardant tout en un seul endroit, CSS-in-JS offre une excellente expérience aux développeurs. Cependant, la plupart des bibliothèques placent l'ensemble de votre CSS dans la balise de style de la page. Cela fonctionne bien dans le navigateur. Cependant, si vous utilisez le rendu côté serveur, vous obtiendrez une page HTML massive et vous paierez en performance pour son rendu. Vous ne recevrez que le CSS dont vous avez besoin, mais il ne pourra pas être mis en cache et le code pour le générer sera inclus dans le bundle JavaScript.

Il existe des bibliothèques CSS-in-JS à temps d'exécution zéro, comme linaria, qui vous permettent d'écrire vos styles dans des fichiers JS et de les extraire dans des fichiers CSS pendant la construction. Sous le capot, elles prennent même en charge le style dynamique basé sur des accessoires via des variables CSS.

4 - Utiliser un rendu côté serveur (SSR) plutot qu'un rendu côté client (CSR)

Les applications côté client typiques exigent que le navigateur récupère, analyse et exécute tout le JavaScript qui génère le document que les utilisateurs voient. Cela prend du temps, surtout si votre appareil est lent.

Le rendu côté serveur offre une bien meilleure expérience aux utilisateurs, car leur navigateur récupère et affiche l'intégralité du document avant de charger, traiter et exécuter votre code JavaScript. Il ne rend pas lentement le DOM sur le client, mais hydrate plutôt l'état. Certes, l'application ne sera pas interactive tout de suite, mais elle sera beaucoup plus rapide. Après le chargement initial, elle se comportera exactement comme une application côté client ordinaire, ce qui signifie que nous ne reviendrons pas à l'époque du code côté serveur qui gaspille les ressources du serveur pour un rendu qui peut être effectué sur le client.

Il utilise le concept React-Hydration (disponible dans Gatsby ou Next.js), qui fournit une fonctionnalité dans laquelle le HTML statique est généré à l'aide de React DOM, puis le contenu est amélioré à l'aide de JS côté client via React Hydration.

Elle réduit considérablement le FCP (First Contentful Paint) et le LCP (Largest Content Paint), qui sont deux des principaux critères du score total de PageSpeed.

Le rendu côté serveur reste relativement lent et consomme des ressources. Gatsby résout ce problème en effectuant un pré-rendu de l'ensemble du site Web en pages HTML statiques. Toutefois, ce n'est pas la meilleure approche pour les sites Web très dynamiques et les applications Web qui dépendent fortement de la saisie de l'utilisateur pour le rendu du contenu.

5 - Optimiser les dépendances NPM

Lorsque vous réfléchissez à la manière de réduire la taille de votre paquet d'applications, tenez compte de la quantité de code que vous utilisez réellement à partir des dépendances. De nombreux projets utilisent encore Moment.js, qui comprend des fichiers localisés pour la prise en charge de plusieurs langues. Si vous n'avez pas besoin de prendre en charge plusieurs langues, vous pouvez supprimer les locales inutilisées de votre paquet final à l'aide du moment-locales-webpack-plugin.

Des paquets sur un tapis roulant

Loadash est un autre exemple. Si vous n'utilisez que 2 des centaines de méthodes, il n'est pas nécessaire d'avoir toutes les méthodes supplémentaires dans votre paquet final.

Pour connaître la taille de l'importation tout de suite dans l'éditeur, utilisez les extensions de code Import Cost VS.

Il est préférable d'utiliser des bibliothèques et des composants qui remplissent la même fonction mais avec une taille de bundle plus petite. Certaines bibliothèques offrent des centaines de variations qui fournissent la même fonctionnalité pour vos applications web.

6. Analyse et optimisation de votre bundle Webpack

Avant de déployer en production, vous devez inspecter et analyser votre bundle d'application pour supprimer tout plugin ou module inutile.

Pour les optimisations, utilisez la bibliothèque webpack-bundle-analyzer, qui affiche la taille des fichiers de sortie de webpack dans un treemap interactif zoomable. Elle crée une visualisation interactive de type treemap de tout le contenu regroupé, vous permettant de voir les modules à l'intérieur et ceux qui prennent le plus de place. Les bundlers modernes prennent en charge un secouage d'arbre (treeshaking) assez sophistiqué qui détecte le code inaccessible et le supprime automatiquement du bundle, mais il faut tout de même y faire attention.

Ce module vous aidera de la manière suivante :

  • Découvrir ce qui se trouve réellement à l'intérieur de votre paquet.
  • Déterminer quels modules consomment le plus d'espace.
  • Trouvez les modules qui se sont retrouvés là par accident.
module.exports = {
      mode: 'production'
    };

7 - Pour finir

Si vous voulez toujours optimiser votre application jusqu'à la mort thermique de l'univers, voici d'autres éléments à prendre en compte :

Définissez les en-têtes Cache-Control

Le champ d'en-tête HTTP Cache-Control contient des directives (instructions) - dans les demandes et les réponses - qui contrôlent la mise en cache dans les navigateurs et les caches partagés. Il aide le navigateur à mettre en cache les ressources et à éviter les demandes multiples au serveur. Il ne réduit pas le temps de chargement initial mais réduit considérablement les temps de chargement ultérieurs de la page Web.

Optimisez les SVG

ils sont évolutifs et se présentent bien dans toutes les dimensions avec une taille de fichier inférieure à celle des mêmes images matricielles (par exemple, JPEG, PNG, etc.). Cependant, certains SVG peuvent contenir beaucoup de déchets qui ne sont pas utilisés pour le rendu mais qui sont quand même téléchargés, ce qui augmente le temps de chargement. La meilleure pratique consiste à utiliser les balises img pour rendre les SVG, mais vous perdez la possibilité de les styliser ou de les animer. Si l'animation et le style sont un cas critique, vous pouvez les utiliser directement.

Utilisez WebP

WebP est un nouveau format d'image proposé par Google pour réduire la taille des fichiers avec une perte de qualité minimale. Il prend en charge la compression avec perte, comme le JPEG, et sans perte, comme le PNG, et vous permet d'économiser beaucoup de bande passante tout en rendant vos utilisateurs et Google heureux avec des temps de chargement plus rapides. En général, les images sont 25 à 35 % plus petites en WebP par rapport à la même qualité en JPEG. Il prend même en charge les animations comme les GIF.

Hébergez vos polices vous-même :

Lorsque vous hébergez vous-même les polices, le navigateur trouve @font-face directement dans le CSS de l'application et n'a plus qu'à télécharger les fichiers de police en utilisant la même connexion à votre serveur ou CDN.

C'est beaucoup plus rapide et cela élimine une seule récupération.