Cinq façons dont AirSHIFT a amélioré les performances d'exécution de son application React

Étude de cas concrète sur l'optimisation des performances des SPA React.

Kento Tsuji
Kento Tsuji
Satoshi Arai
Satoshi Arai
Yusuke Utsunomiya
Yusuke Utsunomiya
Yosuke Furukawa
Yosuke Furukawa

Les performances d'un site Web ne dépendent pas seulement du temps de chargement. Il est essentiel d'offrir une expérience rapide et réactive aux utilisateurs, en particulier pour les applications de bureau de productivité qu'ils utilisent tous les jours. L'équipe d'ingénieurs de Recruit Technologies a mené un projet de refactoring pour améliorer l'une de ses applications Web, AirSHIFT, afin d'améliorer les performances des entrées utilisateur. Voici comment ils ont fait.

Réponse lente, moins de productivité

AirSHIFT est une application Web pour ordinateur de bureau qui aide les propriétaires de magasins, comme les restaurants et les cafés, à gérer les horaires de travail de leur personnel. Créée avec React, l'application monopage propose des fonctionnalités client riches, y compris divers tableaux en grille des plannings de travail organisés par jour, semaine, mois, etc.

Capture d'écran de l'application Web AirSHIFT

À mesure que l'équipe d'ingénieurs de Recruit Technologies ajoutait de nouvelles fonctionnalités à l'application AirSHIFT, elle a commencé à recevoir davantage de commentaires sur les performances lentes. Yosuke Furukawa, responsable de l'ingénierie d'AirSHIFT, a déclaré :

Lors d'une étude de recherche sur l'expérience utilisateur, nous avons été choqués lorsqu'une des propriétaires de magasins a déclaré qu'elle quittait son siège pour préparer du café après avoir cliqué sur un bouton, juste pour tuer le temps en attendant que le tableau de service se charge.

Après avoir examiné les résultats de la recherche, l'équipe d'ingénieurs a constaté que de nombreux utilisateurs essayaient de charger des tables de décalage massives sur des ordinateurs aux spécifications limitées, comme un ordinateur portable Celeron M 1 GHz datant de 10 ans.

Icône de chargement en continu sur les appareils bas de gamme.

L'application AirSHIFT bloquait le thread principal avec des scripts coûteux, mais l'équipe d'ingénierie ne s'est pas rendu compte de leur coût élevé, car elle développait et testait l'application sur des ordinateurs aux spécifications élevées avec des connexions Wi-Fi rapides.

Graphique représentant l'activité d'exécution de l'application.
Lors du chargement de la table "shift", l'exécution de scripts a représenté environ 80% du temps de chargement.

Après avoir profilé ses performances dans Chrome DevTools avec le débit limité du processeur et du réseau activé, il est devenu clair qu'une optimisation des performances était nécessaire. AirSHIFT a constitué une équipe spéciale pour résoudre ce problème. Voici cinq éléments sur lesquels il s'est concentré pour rendre son application plus réactive aux entrées utilisateur.

1. Virtualisation de grandes tables

L'affichage de la table d'équipes a nécessité plusieurs étapes coûteuses: créer le DOM virtuel et l'afficher à l'écran proportionnellement au nombre de membres du personnel et aux créneaux horaires. Par exemple, si un restaurant compte 50 employés et souhaite consulter son planning de travail mensuel, il s'agira d'une table de 50 (membres) multipliés par 30 (jours), ce qui nécessitera de représenter 1 500 composants de cellule. Il s'agit d'une opération très coûteuse, en particulier pour les appareils aux spécifications limitées. En réalité, les choses étaient pires. Il a ainsi appris que certaines boutiques employaient 200 employés, ce qui nécessitait environ 6 000 composants de cellules dans un seul tableau mensuel.

Pour réduire les coûts de cette opération, AirSHIFT a virtualisé la table de rotation. Désormais, l'application installe uniquement les composants dans la fenêtre d'affichage et désinstalle les composants hors écran.

Capture d'écran annotée montrant qu'AirSHIFT utilisait pour afficher du contenu en dehors du viewport.
Avant : affichage de toutes les cellules du tableau de décalage.
Capture d'écran annotée montrant qu'AirSHIFT n'affiche désormais que le contenu visible dans la fenêtre d'affichage.
Après: affiche uniquement les cellules de la fenêtre d'affichage.

Dans ce cas, AirSHIFT a utilisé react-virtualized, car des exigences étaient liées à l'activation de tableaux de grilles bidimensionnels complexes. Ils explorent également des moyens de convertir l'implémentation pour utiliser react-window, une bibliothèque légère.

Résultats

La virtualisation de la table seule a réduit le temps de script de 6 secondes (sur un processeur 4 fois plus lent et un environnement MacBook Pro avec connexion 3G rapide limitée). Il s'agit de l'amélioration des performances la plus efficace du projet de refactorisation.

Capture d'écran annotée d'un enregistrement du panneau "Performances" de Chrome DevTools.
Avant : environ 10 secondes de script après la saisie de l'utilisateur.
Autre capture d'écran annotée d'un enregistrement du panneau "Performances" de Chrome DevTools.
Après : 4 secondes de script après la saisie utilisateur.

2. Auditer avec l'API User Timing

L'équipe AirSHIFT a ensuite refactorisé les scripts exécutés en fonction des entrées utilisateur. Le graphique en flammes des outils pour les développeurs Chrome permet d'analyser ce qui se passe réellement dans le thread principal. Mais l'équipe AirSHIFT a trouvé qu'il était plus facile d'analyser l'activité des applications en fonction du cycle de vie de React.

React 16 fournit sa trace de performances via l'API User Timing, que vous pouvez visualiser dans la section "Timings" des outils pour les développeurs Chrome. AirSHIFT a utilisé la section "Timings" pour trouver une logique inutile exécutée dans les événements de cycle de vie React.

Section "Timings" du panneau "Performances" des outils pour les développeurs Chrome
Les événements de chronométrage utilisateur de React.

Résultats

L'équipe AirSHIFT a découvert qu'une réconciliation d'arbre React inutile se produisait juste avant chaque navigation de parcours. Cela signifiait que React mettait à jour la table de décalage inutilement avant les navigations. Une mise à jour inutile de l'état Redux était à l'origine de ce problème. Cette correction a permis de gagner environ 750 ms de temps de script. AirSHIFT a également effectué d'autres micro-optimisations, ce qui a finalement entraîné une réduction totale de 1 seconde du temps de script.

3. Chargez les composants de manière différée et déplacez la logique coûteuse vers les nœuds Web

AirSHIFT dispose d'une application de chat intégrée. De nombreux propriétaires de magasins communiquent avec leur personnel via le chat tout en consultant le tableau des roulements. Il est donc possible qu'un utilisateur soit en train de taper un message pendant le chargement du tableau. Si le thread principal est occupé par des scripts qui génèrent le tableau, l'entrée utilisateur peut être saccadée.

Pour améliorer cette expérience, AirSHIFT utilise désormais React.lazy et Suspense pour afficher des espaces réservés pour le contenu des tableaux tout en chargeant de manière paresseuse les composants réels.

L'équipe AirSHIFT a également migré une partie de la logique métier coûteuse des composants à chargement différé vers des nœuds de calcul Web. Cela a résolu le problème de à-coups de l'entrée utilisateur en libérant le thread principal afin qu'il puisse se concentrer sur la réponse à l'entrée utilisateur.

En règle générale, les développeurs rencontrent des difficultés à utiliser des workers, mais cette fois, Comlink a fait le gros du travail pour eux. Vous trouverez ci-dessous le pseudo-code montrant comment AirSHIFT a automatisé l'une des opérations les plus coûteuses : le calcul des coûts de main-d'œuvre totaux.

Dans App.js, utilisez React.lazy et Suspense pour afficher le contenu de remplacement pendant le chargement

/** App.js */
import React, { lazy, Suspense } from 'react'

// Lazily loading the Cost component with React.lazy
const Hello = lazy(() => import('./Cost'))

const Loading = () => (
  <div>Some fallback content to show while loading</div>
)

// Showing the fallback content while loading the Cost component by Suspense
export default function App({ userInfo }) {
   return (
    <div>
      <Suspense fallback={<Loading />}>
        <Cost />
      </Suspense>
    </div>
  )
}

Dans le composant "Coût", utilisez comlink pour exécuter la logique de calcul.

/** Cost.js */
import React from 'react';
import { proxy } from 'comlink';

// import the workerlized calc function with comlink
const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
export default async function Cost({ userInfo }) {
  // execute the calculation in the worker
  const instance = await new WorkerlizedCostCalc();
  const cost = await instance.calc(userInfo);
  return <p>{cost}</p>;
}

Implémenter la logique de calcul qui s'exécute dans le worker et l'exposer avec comlink

// WorkerlizedCostCalc.js
import { expose } from 'comlink'
import { someExpensiveCalculation } from './CostCalc.js'

// Expose the new workerlized calc function with comlink
expose({
  calc(userInfo) {
    // run existing (expensive) function in the worker
    return someExpensiveCalculation(userInfo);
  }
}, self);

Résultats

Malgré la quantité limitée de logique qu'ils ont externalisée à titre d'essai, AirSHIFT a déplacé environ 100 ms de code JavaScript du thread principal vers le thread de travail (simulation avec une limitation du processeur x4).

Capture d&#39;écran d&#39;un enregistrement du panneau &quot;Performances&quot; de Chrome DevTools montrant que l&#39;écriture de script se produit désormais sur un worker Web plutôt que sur le thread principal.

AirSHIFT étudie actuellement la possibilité de charger de manière différée d'autres composants et de décharger davantage de logique sur les nœuds de calcul Web afin de réduire davantage les à-coups.

4. Définir un budget de performances

Après avoir implémenté toutes ces optimisations, il était essentiel de s'assurer que l'application reste performante au fil du temps. AirSHIFT utilise désormais bundlesize pour ne pas dépasser la taille actuelle des fichiers JavaScript et CSS. En plus de définir ces budgets de base, il a créé un tableau de bord pour afficher différents percentiles du temps de chargement du tableau des quarts afin de vérifier si l'application est performante même dans des conditions non idéales.

  • Le temps d'exécution du script pour chaque événement Redux est désormais mesuré
  • Les données de performances sont collectées dans Elasticsearch
  • Les performances des 10e, 25e, 50e et 75e centiles de chaque événement sont visualisées avec Kibana.

AirSHIFT surveille désormais l'événement de chargement de la table "Shift" pour s'assurer qu'il se termine en trois secondes pour les utilisateurs du 75e centile. Il s'agit d'un budget non appliqué pour le moment, mais l'entreprise envisage d'envoyer des notifications automatiques via Elasticsearch lorsqu'elle dépasse son budget.

Graphique montrant que le 75e centile est atteint en environ 2 500 ms, le 50e centile en environ 1 250 ms, le 25e centile en environ 750 ms et le 10e centile en environ 500 ms.
Tableau de bord de Kibana affichant les données de performances quotidiennes par centiles.

Résultats

D'après le graphique ci-dessus, AirSHIFT atteint désormais principalement le budget de trois secondes pour les utilisateurs du percentile 75 et charge également le tableau des quarts de travail en moins d'une seconde pour les utilisateurs du percentile 25. En capturant les données de performances RUM dans différentes conditions et sur différents appareils, AirSHIFT peut désormais vérifier si une nouvelle fonctionnalité affecte réellement les performances de l'application ou non.

5. Hackathons sur les performances

Même si tous ces efforts d'optimisation des performances étaient importants et efficaces, il n'est pas toujours facile de demander aux équipes d'ingénierie et d'entreprise de donner la priorité au développement non fonctionnel. Une partie du problème réside dans le fait que certaines de ces optimisations de performances ne peuvent pas être planifiées. Ils nécessitent de l'expérimentation et un état d'esprit d'essai-erreur.

AirSHIFT organise désormais des hackathons internes d'une journée sur les performances pour permettre aux ingénieurs de se concentrer uniquement sur le travail lié aux performances. Dans ces hackathons, toutes les contraintes sont supprimées et la créativité des ingénieurs est respectée. Par conséquent, toute implémentation qui contribue à la rapidité est à envisager. Pour accélérer le hackathon, AirSHIFT divise le groupe en petites équipes, et chaque équipe s'affronte pour déterminer qui peut obtenir la plus grande amélioration du score de performances Lighthouse. Les équipes deviennent très compétitives ! 🔥

Photos du hackathon

Résultats

L'approche du hackathon fonctionne bien pour eux.

  • Les goulots d'étranglement des performances peuvent être facilement détectés en essayant plusieurs approches pendant le hackathon et en mesurant chacune d'elles avec Lighthouse.
  • Après le hackathon, il est assez facile de convaincre l'équipe de l'optimisation qu'elle doit privilégier pour la publication en production.
  • C'est également un moyen efficace de promouvoir l'importance de la vitesse. Chaque participant peut comprendre la corrélation entre la façon dont vous codez et les performances obtenues.

Un bon effet secondaire a été que de nombreuses autres équipes d'ingénieurs de Recruit se sont intéressées à cette approche pratique. L'équipe AirSHIFT organise désormais plusieurs hackathons rapides au sein de l'entreprise.

Résumé

Ce n'a pas été le parcours le plus facile pour AirSHIFT de travailler sur ces optimisations, mais cela a porté ses fruits. AirSHIFT charge désormais le tableau des quarts de travail en 1,5 seconde en moyenne, ce qui représente une amélioration de 6 fois par rapport à ses performances avant le projet.

Après le lancement des optimisations des performances, un utilisateur a déclaré :

Merci beaucoup d'avoir accéléré le chargement du tableau des quarts. La planification des équipes est désormais beaucoup plus efficace.