Optimiser les performances

En interne, React fait appel à différentes techniques intelligentes pour minimiser le nombre d’opérations coûteuses sur le DOM nécessaires à la mise à jour de l’interface utilisateur (UI). Pour de nombreuses applications, utiliser React offrira une UI rapide sans avoir à fournir beaucoup de travail pour optimiser les performances. Néanmoins, il existe plusieurs façons d’accélérer votre application React.

Utiliser la version de production

Si vous mesurez ou rencontrez des problèmes de performances dans vos applications React, assurez-vous que vous testez bien la version minifiée de production.

Par défaut, React intègre de nombreux avertissements pratiques. Ces avertissements sont très utiles lors du développement. Toutefois, ils rendent React plus gros et plus lent, vous devez donc vous assurer que vous utilisez bien une version de production lorsque vous déployez l’application.

Si vous n’êtes pas sûr·e que votre processus de construction est correctement configuré, vous pouvez le vérifier en installant l’extension React Developer Tools pour Chrome. Si vous visitez un site avec React en production, l’icône aura un fond sombre :

React DevTools sur un site web utilisant la version de production de React

Si vous visitez un site avec React dans sa version de développement, l’icône aura un fond rouge :

React DevTools sur un site web utilisant la version de développement de React

L’idée, c’est que vous utilisiez le mode développement lorsque vous travaillez sur votre application, et le mode production lorsque vous la déployez pour vos utilisateurs.

Vous trouverez ci-dessous les instructions pour procéder à la construction de votre application pour la production.

Create React App

Si votre projet est construit avec Create React App, exécutez :

npm run build

Cela génèrera la version de production de votre application dans le répertoire build/ de votre projet.

Rappelez-vous que cela n’est nécessaire qu’avant le déploiement en production. Lors du développement, utilisez npm start.

Versions de production officielles

Nous mettons à disposition des versions de React et de React DOM prêtes pour la production sous la forme de fichiers uniques :

<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

Rappelez-vous que seuls les fichiers React finissant par .production.min.js sont adaptés à la production.

Brunch

Pour obtenir la version de production la plus efficace avec Brunch, installez l’extension terser-brunch :

# Si vous utilisez npm :
npm install --save-dev terser-brunch

# Si vous utilisez Yarn :
yarn add --dev terser-brunch

Ensuite, pour créer la version de production, ajoutez l’option -p à la commande build :

brunch build -p

N’oubliez pas que cela n’est nécessaire que pour générer votre version de production. Vous ne devez pas utiliser l’argument -p ni l’extension lors des phases de développement, car ça masquerait les avertissements utiles de React et ralentirait notablement la construction de l’application.

Browserify

Pour obtenir la version de production la plus efficace avec Browserify, installez quelques extensions :

# Si vous utilisez npm :
npm install --save-dev envify terser uglifyify

# Si vous utilisez Yarn :
yarn add --dev envify terser uglifyify

Pour créer la version de production, assurez-vous d’ajouter ces transformations (l’ordre a son importance) :

  • La transformation envify s’assure que l’environnement est correctement défini. Définissez-la globalement (-g).
  • La transformation uglifyify supprime les imports de développement. Définissez-la également au niveau global (-g).
  • Enfin, le bundle qui en résulte est transmis à terser pour être obfusqué (les raisons sont détaillées ici).

Par exemple :

browserify ./index.js \
  -g [ envify --NODE_ENV production ] \
  -g uglifyify \
  | terser --compress --mangle > ./bundle.js

Rappelez-vous que vous n’avez à faire cela que pour la version de production. Vous ne devez pas appliquer ces extensions en développement, car cela masquerait des avertissements utiles de React et ralentirait la construction.

Rollup

Pour obtenir la version de production la plus efficace avec Rollup, installez quelques extensions :

# Si vous utilisez npm :
npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser

# Si vous utilisez Yarn :
yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser

Pour créer la version de production, assurez-vous d’ajouter ces transformations (l’ordre a son importance) :

  • L’extension replace s’assure que l’environnement est correctement configuré.
  • L’extension commonjs prend en charge CommonJS au sein de Rollup.
  • L’extension terser réalise la compression et obfusque le bundle final.
plugins: [
  // ...
  require('rollup-plugin-replace')({
    'process.env.NODE_ENV': JSON.stringify('production')
  }),
  require('rollup-plugin-commonjs')(),
  require('rollup-plugin-terser')(),
  // ...
]

Pour une configuration complète, vous pouvez consulter ce gist.

Rappelez-vous que vous n’avez à faire cela que pour la version de production. Vous ne devez pas utiliser les extensions terser ou replace avec une valeur 'production' en développement, car cela masquerait des avertissements utiles de React et ralentirait la construction.

webpack

Remarque

Si vous utilisez Create React App, merci de suivre les instructions ci-dessus.
Cette section n’est utile que si vous configurez webpack vous-même.

Webpack v4+ minifera automatiquement votre code en mode production.

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production'
  optimization: {
    minimizer: [new TerserPlugin({ /* additional options here */ })],
  },
};

Vous pouvez en apprendre davantage sur le sujet en consultant la documentation webpack.

Rappelez-vous que vous n’avez à faire cela que pour la version de production. Vous ne devez pas utiliser TerserPlugin en développement, car cela masquerait des avertissements utiles de React et ralentirait la construction.

Profilage des composants avec l’onglet Performance de Chrome

En mode de développement, vous pouvez voir comment les composants sont montés, mis à jour et démontés en utilisant les outils de performances dans les navigateurs qui les prennent en charge. Par exemple :

Des composants React dans la frise chronologique de Chrome

Pour faire ça avec Chrome :

  1. Désactivez temporairement toutes les extensions de Chrome, en particulier React DevTools. Elles peuvent considérablement impacter les résutats !

  2. Assurez-vous d’utiliser l’application en mode de développement.

  3. Ouvrez l’onglet Performances dans les DevTools de Chrome et appuyez sur Record.

  4. Effectuez les opérations que vous voulez analyser. N’enregistrez pas plus de 20 secondes, car Chrome pourrait se bloquer.

  5. Arrêtez l’enregistrement.

  6. Les événements React seront regroupés sous l’étiquette User Timing.

Pour une présentation plus détaillée, consultez cet article de Ben Schwarz.

Veuillez noter que ces résultats sont relatifs et que les composants seront rendus plus rapidement en production. Néanmoins, ça devrait vous aider à comprendre quand des éléments d’interface sont mis à jour par erreur, ainsi que la profondeur et la fréquence des mises à jour de l’UI.

Pour le moment, Chrome, Edge et IE sont les seuls navigateurs prenant en charge cette fonctionnalité, mais comme nous utilisons l’API standard User Timing, nous nous attendons à ce que d’autres navigateurs la prennent en charge.

Profilage des composants avec le DevTools Profiler

react-dom 16.5+ et react-native 0.57+ offrent des capacités de profilage avancées en mode de développement avec le Profiler de l’extension React DevTools. Vous trouverez un aperçu du Profiler sur le billet de blog “Introducing the React Profiler”. Une présentation vidéo du Profiler est également disponible sur YouTube.

Si vous n’avez pas encore installé l’extension React DevTools, vous pourrez la trouver ici :

Remarque

Un module de profilage pour la production de react-dom existe aussi dans react-dom/profiling. Pour en savoir plus sur l’utilisation de ce module, rendez-vous à l’adresse fb.me/react-profiling.

Virtualiser les listes longues

Si votre application génère d’importantes listes de données (des centaines ou des milliers de lignes), nous vous conseillons d’utiliser la technique de « fenêtrage » (windowing, NdT). Cette technique consiste à n’afficher à tout instant qu’un petit sous-ensemble des lignes, ce qui permet de diminuer considérablement le temps nécessaire au rendu des composants ainsi que le nombre de nœuds DOM créés.

react-window et react-virtualized sont des bibliothèques populaires de gestion du fenêtrage. Elles fournissent différents composants réutilisables pour afficher des listes, des grilles et des données tabulaires. Vous pouvez également créer votre propre composant, comme l’a fait Twitter, si vous voulez quelque chose de plus adapté à vos cas d’usage spécifiques.

Éviter la réconciliation

React construit et maintient une représentation interne de l’UI produite, représentation qui inclut les éléments React renvoyés par vos composants. Elle permet à React d’éviter la création de nœuds DOM superflus et l’accès excessif aux nœuds existants, dans la mesure où ces opérations sont plus lentes que sur des objets JavaScript. On y fait parfois référence en parlant de « DOM virtuel », mais ça fonctionne de la même façon avec React Native.

Quand les props ou l’état local d’un composant changent, React décide si une mise à jour du DOM est nécessaire en comparant l’élément renvoyé avec l’élément du rendu précédent. Quand ils ne sont pas égaux, React met à jour le DOM.

Vous pouvez visualiser ces rendus de mise à jour du DOM virtuel avec React DevTools :

Dans la console de développement, choisissez l’option Highlight Updates dans l’onglet React :

Comment activer l'option

Interagissez avec votre page, et vous devriez voir des bordures colorées apparaître momentanément autour des composants dont le rendu est mis à jour. Ça vous permet de détecter les mises à jour inutiles. Vous pouvez en apprendre davantage sur cette fonctionnalité des React DevTools en lisant ce billet du blog de Ben Edelstein.

Prenons cet exemple :

Exemple de la fonctionnalité de mise en évidence des mises à jour avec React DevTools

Remarquez que lorsque l’on saisit une seconde tâche, la première clignote également à l’écran à chaque frappe. Ça signifie qu’elle est également rafraîchie par React avec son champ de saisie. On parle parfois de rendu « gâché ». Nous savons que c’est inutile car le contenu de la première tâche est inchangé, mais React l’ignore.

Même si React ne met à jour que les nœuds DOM modifiés, refaire un rendu prend un certain temps. Dans la plupart des cas ce n’est pas un problème, mais si le ralentissement est perceptible, vous pouvez accélérer le processus en surchargeant la méthode shouldComponentUpdate du cycle de vie, qui est déclenchée avant le démarrage du processus de rafraîchissement. L’implémentation par défaut de cette méthode renvoie true, laissant ainsi React faire la mise à jour :

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

Si vous savez que dans certaines situations votre composant n’a pas besoin d’être mis à jour, vous pouvez plutôt renvoyer false depuis shouldComponentUpdate afin de sauter le rendu, et donc l’appel à la méthode render() sur ce composant et ses enfants.

Le plus souvent, plutôt que d’écrire manuellement shouldComponentUpdate(), vous pouvez plutôt choisir d’étendre React.PureComponent. Ça revient à implémenter shouldComponentUpdate() avec une comparaison superficielle des propriétés et état actuels et précédents.

shouldComponentUpdate en action

Voici un sous-arbre de composants. Pour chacun, SCU indique ce que shouldComponentUpdate renvoie, et vDOMEq indique si les éléments renvoyés étaient équivalents. Enfin, la couleur du cercle indique si le composant doit être réconcilié ou non.

Arbre des composants montrant l'utilisation de shouldComponentUpdate

Puisque shouldComponentUpdate a renvoyé false pour le sous-arbre d’origine C2, React n’a pas tenté de faire le rendu de C2, et n’a pas invoqué non plus shouldComponentUpdate sur C4 et C5.

Pour C1 et C3, shouldComponentUpdate a renvoyé true, React a donc dû descendre dans les feuilles de l’arbre et les vérifier. Pour C6, shouldComponentUpdate a renvoyé true, et puisque les éléments renvoyés n’étaient pas équivalents, React a dû mettre à jour le DOM.

Le dernier cas intéressant concerne C8. React a dû faire le rendu de ce composant, mais puisque les éléments React renvoyés étaient équivalents à ceux du rendu précédent, il n’était pas nécessaire de mettre à jour le DOM.

Remarquez que React n’a dû modifier le DOM que pour C6, ce qui était inévitable. Pour C8, il s’en est dispensé suite à la comparaison des éléments React renvoyés, et pour le sous-arbre de C2 ainsi que pour C7, il n’a même pas eu à comparer les éléments car nous avons abandonné au niveau de shouldComponentUpdate, et render n’a pas été appelée.

Exemples

Si la seule façon de changer pour votre composant provient d’une modification de props.color ou state.count, alors vous devez vérifier ces valeurs dans shouldComponentUpdate :

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Compteur : {this.state.count}
      </button>
    );
  }
}

Dans ce code, shouldComponentUpdate vérifie simplement si props.color ou state.count ont changé. Dans le cas contraire, le composant n’est pas mis à jour. Si votre composant devient plus complexe, vous pourriez utiliser une approche similaire en procédant à une « comparaison superficielle » (shallow comparison, NdT) de tous les champs de props et state afin de déterminer si le composant doit être mis à jour. Ce modèle est suffisamment fréquent pour que React nous y aide : on hérite simplement de React.PureComponent. Ce code est donc une façon plus simple de réaliser la même chose :

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Compteur : {this.state.count}
      </button>
    );
  }
}

La plupart du temps, vous pouvez utiliser React.PureComponent au lieu de redéfinir shouldComponentUpdate vous-même. Il ne réalise qu’une comparaison superficielle, vous ne pouvez donc pas l’utiliser si les propriétés ou l’état sont modifiés d’une façon qui échapperait à ce type de comparaison.

Ça peut devenir un problème avec des structures de données plus complexes. Supposons, par exemple, que vous voulez qu’un composant ListOfWords affiche une liste de mots séparés par des virgules, avec un composant parent WordAdder qui vous permet d’ajouter un mot à la liste d’un simple clic. Ce code ne fonctionnera pas correctement :

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // Cette section comporte une mauvaise pratique qui entraînera un bug.
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

Le problème est que PureComponent va faire une comparaison référentielle entre l’ancienne et la nouvelle valeur de this.props.words. Dans la mesure où ce code modifie directement le tableau words dans la méthode handleClick de WordAdder, l’ancienne et la nouvelle valeurs de this.props.words sont considérées comme équivalentes (même objet en mémoire), bien que les mots dans le tableau aient été modifiés. Le composant ListOfWords ne sera pas mis à jour, même s’il devrait afficher de nouveaux mots.

La puissance des données immuables

La façon la plus simple d’éviter ce problème consiste à éviter de modifier directement les valeurs que vous utilisez dans les props ou l’état local. Par exemple, la méthode handleClick au-dessus pourrait être réécrite en utilisant concat comme suit :

handleClick() {
  this.setState(state => ({
    words: state.words.concat(['marklar'])
  }));
}

ES6 offre la syntaxe de décomposition (spread operator, NdT) pour les tableaux, ce qui facilite ce type d’opération. Si vous utilisez Create React App, cette syntaxe est disponible par défaut.

handleClick() {
  this.setState(state => ({
    words: [...state.words, 'marklar'],
  }));
};

D’une manière similaire, vous pouvez réécrire du code qui modifie des objets en évitant la mutation. Par exemple, supposons que nous ayons un objet nommé colormap et que nous voulions écrire une fonction qui change la valeur de colormap.right en 'blue'. Nous pourrions l’écrire ainsi :

function updateColorMap(colormap) {
  colormap.right = 'blue';
}

Pour écrire cela en évitant de modifier l’objet original, nous pouvons utiliser la méthode Object.assign :

function updateColorMap(colormap) {
  return Object.assign({}, colormap, {right: 'blue'});
}

updateColorMap renvoie désormais un nouvel objet, plutôt que de modifier l’ancien. Object.assign fait partie d’ES6 et nécessite un polyfill.

Il existe une proposition JavaScript pour ajouter la décomposition des propriétés d’objet (object spread properties, NdT) afin de simplifier la mise à jour des objets sans pour autant les modifier :

function updateColorMap(colormap) {
  return {...colormap, right: 'blue'};
}

Si vous utilisez Create React App, la méthode Object.assign et la syntaxe de décomposition d’objets sont toutes deux disponibles par défaut.

Utiliser des structures de données immuables

L’utilisation d’Immutable.js est une autre façon de résoudre ce problème. Elle fournit des collections immuables et persistantes qui fonctionnent avec du partage structurel :

  • Immuables : une fois créée, une collection ne peut plus être modifiée ultérieurement.
  • Persistantes : de nouvelles collections peuvent être créées à partir d’une ancienne collection et d’une mutation telle que set. La collection d’origine reste valide une fois la nouvelle collection créée.
  • Partage structurel : les nouvelles collections sont créées en utilisant au maximum la structure de la collection d’origine, réduisant la copie au minimum pour améliorer les performances.

L’immutabilité rend le suivi des modifications peu coûteux. Un changement résultera toujours en un nouvel objet, nous n’avons alors qu’à vérifier si la référence de l’objet a changé. Par exemple, prenons ce code JavaScript classique :

const x = { foo: 'bar' };
const y = x;
y.foo = 'baz';
x === y; // true

Bien que y ait été modifié, vu qu’il s’agit toujours d’une référence au même objet x, cette comparaison renverra true. Vous pouvez écrire un code similaire avec immutable.js :

const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
const z = x.set('foo', 'bar');
x === y; // false
x === z; // true

Dans ce cas, puisqu’une nouvelle référence est renvoyée quand on modifie x, nous pouvons utiliser la vérification d’égalité référentielle (x === y) pour vérifier que la nouvelle valeur stockée dans y est différente de celle d’origine stockée dans x.

D’autres bibliothèques facilitent l’utilisation des données immuables, notamment Immer, immutability-helper, and seamless-immutable.

Les structures de données immuables vous offrent un moyen peu coûteux de suivre les modifications apportées aux objets. C’est tout ce dont nous avons besoin pour implémenter la méthode shouldComponentUpdate. Ça peut souvent contribuer à améliorer significativement les performances.