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

Étude de cas concret sur l'optimisation des performances de l'application monopage React

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

Les performances d'un site Web ne se résument pas au temps de chargement. Il est essentiel de proposer une expérience rapide et réactive aux utilisateurs, en particulier pour les applications de productivité de bureau que les utilisateurs utilisent tous les jours. L'équipe d'ingénieurs de Recruit Technologies a participé à un projet de refactorisation afin d'améliorer l'une de ses applications Web, AirSHIFT, afin d'optimiser les performances des entrées utilisateur. Voici comment ils ont fait.

Temps de réponse lent, productivité réduite

AirSHIFT est une application Web de bureau qui aide les propriétaires de magasins, comme les restaurants et les cafés, à gérer le travail posté de leur personnel. Conçue avec React, cette application monopage fournit de nombreuses fonctionnalités client, y compris diverses tables sous forme de grille des plannings d'équipes organisées 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 concernant la lenteur des performances. Yosuke Furukawa, responsable de l'ingénierie chez AirSHIFT, déclare:

Lors d'une étude sur l'expérience utilisateur, nous avons été choqués lorsque l'une des propriétaires du magasin a déclaré qu'elle quitterait son siège pour préparer du café après avoir cliqué sur un bouton, simplement pour faire passer le temps à attendre que la table d'équipes se charge.

Après examen, l'équipe d'ingénieurs s'est rendu compte que bon nombre de ses utilisateurs essayaient de charger d'énormes tables de travail sur des ordinateurs basse consommation, tels qu'un ordinateur portable Celeron M 1 GHz d'il y a 10 ans.

Icône de chargement sans fin sur les appareils bas de gamme.

L'application AirSHIFT bloquait le thread principal avec des scripts coûteux, mais l'équipe d'ingénierie n'avait pas réalisé le coût de ces scripts, car ils développaient et testaient des ordinateurs aux spécifications riches, dotés d'une connexion Wi-Fi rapide.

Graphique représentant l'activité d'exécution de l'application.
Lors du chargement de la table Shift, environ 80% du temps de chargement a été consommé par l'exécution de scripts.

Après avoir profilé leurs performances dans les outils pour les développeurs Chrome avec la limitation du processeur et du réseau activée, il est devenu évident que l'optimisation des performances était nécessaire. AirSHIFT a mis en place un groupe de travail pour s'attaquer à ce problème. Voici cinq points sur lesquels ils se sont concentrés pour rendre leur application plus réactive aux entrées utilisateur.

1. Virtualiser de grandes tables

L'affichage de la table des équipes nécessitait plusieurs étapes coûteuses: construire 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 membres actifs et veut vérifier son planning mensuel, il s’agirait d’un tableau de 50 (membres) multiplié par 30 (jours), ce qui donnerait l’affichage de 1 500 composants de cellule. Il s'agit d'une opération très coûteuse, en particulier pour les appareils peu performants. En réalité, la situation était pire. Grâce à cette étude, ils ont appris que des magasins géraient 200 employés,nécessitant environ 6 000 éléments de cellules dans une seule table mensuelle.

Pour réduire le coût de cette opération, AirSHIFT a virtualisé la table de décalages. 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 qui montre que la fonctionnalité AirSHIFT a été utilisée pour afficher le contenu en dehors de la fenêtre d'affichage.
Avant: affichage de toutes les cellules du tableau décalé
Capture d'écran annotée qui montre que AirSHIFT affiche désormais uniquement le contenu visible dans la fenêtre d'affichage.
Après: affichage des cellules dans la fenêtre d'affichage uniquement.

Dans ce cas, AirSHIFT a utilisé la réaction virtualisée, car il y avait des exigences liées à l'activation de tables en grille bidimensionnelles complexes. Elle étudie également des moyens de convertir l'implémentation pour utiliser la fenêtre de réaction allégée react-window à l'avenir.

Résultats

La virtualisation de la table à elle seule a permis de réduire de six secondes le temps d'écriture des scripts (sur un processeur Macbook Pro multiplié par quatre et un Macbook Pro avec une limitation rapide de la 3G). Il s'agit de l'amélioration des performances la plus significative dans le projet de refactorisation.

Capture d'écran annotée d'un enregistrement du panneau "Performances" des outils pour les développeurs Chrome.
Avant: environ 10 secondes de script après une entrée utilisateur.
Autre capture d'écran annotée d'un enregistrement du panneau "Performances" des outils pour les développeurs Chrome.
Après: quatre secondes d'écriture de script après la saisie de l'utilisateur.

2. Auditer avec l'API User Timing

Ensuite, l'équipe AirSHIFT a refactorisé les scripts qui s'exécutent sur les entrées utilisateur. Le graphique de type "flamme" des outils pour les développeurs Chrome permet d'analyser ce qui se passe réellement dans le thread principal. Toutefois, 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 "Durée" pour détecter une logique inutile s'exécutant dans les événements de cycle de vie React.

Section "Délais" du panneau "Performances" des outils pour les développeurs Chrome
Événements de temps utilisateur de React.

Résultats

L'équipe AirSHIFT a découvert qu'une réconciliation React Tree inutile se produisait juste avant chaque navigation. Cela signifiait que React mettait inutilement à jour la table Shift avant les navigations. Ce problème était dû à une mise à jour inutile de l'état Redux. Sa correction a permis d'économiser environ 750 ms de temps de script. AirSHIFT a également effectué d'autres micro-optimisations, ce qui a fini par réduire le temps d'écriture du script d'une seconde au total.

3. Composants de chargement différé et transfert d'une logique coûteuse vers des workers Web

AirSHIFT intègre une application de chat. De nombreux propriétaires de magasins communiquent avec les membres de leur personnel via le chat lorsqu'ils consultent la table d'équipes, ce qui signifie qu'un utilisateur peut être en train de taper un message pendant le chargement de la table. Si le thread principal est occupé par des scripts qui affichent la table, 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 d'une table lors du chargement différé des composants.

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 d'à-coups dans les entrées utilisateur en libérant le thread principal pour qu'il puisse se concentrer sur la réponse à l'entrée utilisateur.

Généralement, les développeurs sont confrontés à la complexité de l'utilisation des nœuds de calcul, mais cette fois, Comlink s'est chargé du plus gros du travail pour eux. Le pseudo-code ci-dessous explique comment AirSHIFT a traité l'une des opérations les plus coûteuses de la société: 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>
  )
}

Utiliser comlink pour exécuter la logique de calcul dans le composant "Coût"

/** 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é le nombre limité de logiques utilisées pour les essais, AirSHIFT a déplacé environ 100 ms de son code JavaScript du thread principal vers le thread de travail (simulé avec une limitation du processeur multipliée par quatre).

Capture d&#39;écran d&#39;un enregistrement du panneau &quot;Performances&quot; des outils pour les développeurs Chrome montrant que l&#39;exécution de script est désormais effectuée sur un worker Web plutôt que sur le thread principal.

AirSHIFT étudie actuellement s'il est possible de charger d'autres composants de manière différée et de décharger davantage de logique vers les workers 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 restait 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, l'équipe a créé un tableau de bord affichant différents centiles du temps de chargement de la table d'ajustements 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 de chaque événement aux 10, 25, 50 et 75e centiles 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 se termine 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 Kibana affichant les données de performances quotidiennes par centile.

Résultats

Le graphique ci-dessus montre qu'AirSHIFT atteint désormais principalement le budget de 3 secondes pour les utilisateurs du 75e centile et qu'il charge également la table des changements en une seconde pour les utilisateurs du 25e centile. En capturant les données de performances RUM de plusieurs conditions et appareils, AirSHIFT peut désormais vérifier si une nouvelle version de fonctionnalité affecte réellement les performances de l'application.

5. Hackathons de 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 commerciales de donner la priorité au développement non fonctionnel. Une partie du défi réside dans le fait que certaines de ces optimisations de performances ne peuvent pas être planifiées. Elles nécessitent d'expérimenter et d'adopter une approche d'essais et d'erreurs.

AirSHIFT mène désormais des hackathons internes d'une journée pour permettre aux ingénieurs de se concentrer uniquement sur les tâches liées aux performances. Dans ces hackathons, ils éliminent toutes les contraintes et respectent la créativité des ingénieurs, ce qui signifie que toute implémentation qui contribue à la vitesse mérite d'être envisagée. Pour accélérer le hackathon, AirSHIFT divise le groupe en petites équipes. Chacune d'entre elles s'affronte afin de déterminer laquelle obtiendra le meilleur score de performances Lighthouse. Les équipes deviennent très compétitives ! 🔥

Photos du hackathon.

Résultats

L'approche du hackathon fonctionne bien pour eux.

  • Pour détecter facilement les goulots d'étranglement qui affectent les performances, testez plusieurs approches pendant le hackathon et mesurez chacune d'elles avec Lighthouse.
  • Après le hackathon, il est assez facile de convaincre l'équipe de l'optimisation à privilégier pour la sortie de production.
  • C'est aussi un moyen efficace de défendre l'importance de la vitesse. Chaque participant peut comprendre la corrélation entre la façon dont vous codez et les résultats qu'elle génère.

L'avantage, c'est que de nombreuses autres équipes d'ingénieurs de Recruit se sont intéressées à cette approche pratique et que l'équipe AirSHIFT organise désormais plusieurs hackathons de vitesse au sein de l'entreprise.

Résumé

Travailler sur ces optimisations n'a certainement pas été le parcours le plus simple pour AirSHIFT, mais cela a certainement porté ses fruits. Désormais, AirSHIFT charge la table d'évolution en 1,5 seconde en médiane, ce qui représente une amélioration de ses performances par rapport à celle du projet avant le projet.

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

Merci beaucoup d'avoir accéléré le chargement de la table Shift. Organiser le travail par roulement est beaucoup plus efficace maintenant.