Cinque modi in cui AirSHIFT ha migliorato le prestazioni di runtime della loro app React

Un case study reale sull'ottimizzazione delle prestazioni di React SPA.

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

Il rendimento del sito web non riguarda solo il tempo di caricamento. È fondamentale offrire agli utenti un'esperienza veloce e reattiva, in particolare per le app desktop di produttività che le persone usano tutti i giorni. Il team di ingegneri di Recruit Technologies ha intrapreso un progetto di refactoring per migliorare una delle sue app web, AirSHIFT, in modo da migliorare le prestazioni dell'input utente. Ecco come ci sono riusciti.

Risposta lenta, meno produttività

AirSHIFT è un'applicazione web desktop che aiuta i proprietari di negozi, come ristoranti e bar, a gestire il lavoro a turni del personale. Realizzata con React, l'applicazione a pagina singola offre funzionalità avanzate per il client, tra cui varie tabelle a griglia dei programmi dei turni organizzati per giorno, settimana, mese e altro ancora.

Uno screenshot dell'app web AirSHIFT.

Quando il team di tecnici di Recruit Technologies ha aggiunto nuove funzionalità all'app AirSHIFT, ha iniziato a vedere più feedback sulle prestazioni lente. L'ingegner diretto di AirSHIFT, Yosuke Furukawa, ha dichiarato:

In uno studio di ricerca con gli utenti, siamo rimasti sorpresi quando uno dei proprietari di negozi ha detto che avrebbe lasciato il suo posto per andare a prendere il caffè dopo aver fatto clic su un pulsante, solo per finire di aspettare che il tavolino si caricasse.

Dopo avere svolto la ricerca, il team di tecnici si è reso conto che molti dei loro utenti cercavano di caricare enormi tabelle di turni su computer con specifiche basse, ad esempio un laptop Celeron M da 1 GHz di dieci anni fa.

Rotellina senza fine su dispositivi di fascia bassa.

L'app AirSHIFT bloccava il thread principale con script costosi, ma il team di tecnici non si era reso conto di quanto fossero costosi gli script perché stavano sviluppando e testando su computer con specifiche complete con connessioni Wi-Fi veloci.

Un grafico che mostra l'attività di runtime dell'app.
Durante il caricamento della tabella di spostamento, l'esecuzione degli script ha consumato circa l'80% del tempo di caricamento.

Dopo aver profilato le prestazioni in Chrome DevTools con la limitazione di CPU e rete abilitata, è diventato chiaro che era necessaria l'ottimizzazione delle prestazioni. AirSHIFT ha formato una task force per affrontare questo problema. Ecco 5 aspetti su cui si è concentrata per rendere l'app più reattiva in base all'input degli utenti.

1. Virtualizza tabelle di grandi dimensioni

La visualizzazione della tabella delle turni ha richiesto diversi passaggi costosi: la creazione del DOM virtuale e il suo rendering sullo schermo in proporzione al numero di membri dello staff e alle fasce orarie. Ad esempio, se un ristorante aveva 50 dipendenti e volesse controllare il programma dei turni mensili, sarebbe una tabella di 50 (membri) moltiplicata per 30 (giorni) che porterebbe a 1.500 componenti di celle da visualizzare. Si tratta di un'operazione molto costosa, soprattutto per i dispositivi con specifiche basse. In realtà, le cose andavano peggio. Dalla ricerca hanno appreso che c'erano negozi che gestivano 200 membri del personale, che richiedevano circa 6.000 componenti cellulari in un'unica tabella mensile.

Per ridurre il costo di questa operazione, AirSHIFT ha virtualizzato la tabella delle turni. L'app ora monta solo i componenti all'interno dell'area visibile e smonta i componenti esterni allo schermo.

Uno screenshot annotato che dimostra che AirSHIFT è stato utilizzato per visualizzare i contenuti al di fuori dell'area visibile.
Prima: rendering di tutte le celle della tabella delle variazioni.
Uno screenshot annotato che dimostra che AirSHIFT ora mostra solo i contenuti visibili nell'area visibile.
Dopo: visualizza solo le celle all'interno dell'area visibile.

In questo caso, AirSHIFT ha utilizzato react-virtualized in quanto erano previsti requisiti per l'abilitazione di tabelle a griglia bidimensionali complesse. Stanno anche valutando dei modi per convertire l'implementazione in modo da utilizzare la leggera react-window in futuro.

Risultati

La virtualizzazione della sola tabella ha ridotto il tempo di scripting di 6 secondi (in un rallentamento della CPU 4x + un ambiente Macbook Pro throttled 3G veloce). Questo è stato il miglioramento delle prestazioni di maggior impatto nel progetto di refactoring.

Uno screenshot annotato di una registrazione del riquadro Prestazioni di Chrome DevTools.
Prima: circa 10 secondi di scripting dopo l'input dell'utente.
Un altro screenshot annotato di una registrazione del riquadro Prestazioni di Chrome DevTools.
Dopo: 4 secondi di scripting dopo l'input dell'utente.

2. Controllo con l'API User Timing

Successivamente, il team di AirSHIFT ha effettuato il refactoring degli script eseguiti sulla base dell'input dell'utente. Il fiamma a fiamma di Chrome DevTools consente di analizzare ciò che sta accadendo effettivamente nel thread principale. Ma il team di AirSHIFT ha trovato più facile analizzare l'attività delle applicazioni in base al ciclo di vita di React.

React 16 fornisce informazioni sulle prestazioni tramite l'API User Timing, che puoi visualizzare dalla sezione Tempi di Chrome DevTools. AirSHIFT ha utilizzato la sezione Tempi per trovare la logica non necessaria in esecuzione negli eventi del ciclo di vita di React.

La sezione Tempi del riquadro Prestazioni di Chrome DevTools.
Eventi di tempo utente di React.

Risultati

Il team di AirSHIFT ha scoperto che era in corso un'inutile React Tree Reconciliation appena prima di ogni navigazione del percorso. Ciò significa che React aggiornava inutilmente la tabella delle turni prima delle navigazioni. Il problema era causato da un aggiornamento non necessario dello stato Redux. La correzione ha consentito di risparmiare circa 750 ms di tempo per lo scripting. AirSHIFT ha apportato anche altre micro ottimizzazioni, che alla fine hanno portato a una riduzione totale di un secondo del tempo di scripting.

3. Caricamento lento dei componenti e trasferimento di logiche costose ai worker web

AirSHIFT ha un'applicazione di chat integrata. Molti proprietari di negozi comunicano con i membri del personale tramite la chat mentre guardano la tabella dei turni, il che significa che un utente potrebbe digitare un messaggio mentre la tabella è in fase di caricamento. Se il thread principale contiene script che eseguono il rendering della tabella, l'input utente potrebbe essere scadente.

Per migliorare questa esperienza, AirSHIFT ora utilizza React.lazy e Suspense per mostrare i segnaposto per i contenuti dei tabelle durante il caricamento lento dei componenti effettivi.

Il team di AirSHIFT ha anche migrato alcune delle costose logiche di business all'interno dei componenti caricati lentamente ai web worker. Questo ha risolto il problema di jank dell'input utente liberando il thread principale in modo che potesse concentrarsi sulla risposta all'input dell'utente.

Di solito gli sviluppatori si trovano ad affrontare complessità nell'utilizzo dei worker, ma questa volta è Comlink a occuparsi della maggior parte del lavoro. Di seguito è riportato lo pseudocodice di come AirSHIFT ha lavorato su una delle operazioni più costose che avrebbero avuto: il calcolo del costo totale del lavoro.

In App.js, usa React.lazy e Suspense per mostrare i contenuti di fallback durante il caricamento

/** 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>
  )
}

Nel componente di costo, utilizza comlink per eseguire la logica di calcolo.

/** 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>;
}

Implementa la logica di calcolo che viene eseguita nel worker ed esponila con 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);

Risultati

Nonostante la quantità limitata di logica che ha worker per la prova, AirSHIFT ha spostato circa 100 ms del codice JavaScript dal thread principale al thread worker (simulato con una limitazione della CPU 4x).

Uno screenshot di una registrazione del riquadro Prestazioni di Chrome DevTools che mostra che lo scripting è ora in corso su un web worker anziché nel thread principale.

AirSHIFT sta attualmente valutando se sia possibile eseguire il caricamento lento di altri componenti e trasferire più logica ai web worker per ridurre ulteriormente il jank.

4. Impostare un budget delle prestazioni

Dopo aver implementato tutte queste ottimizzazioni, era fondamentale assicurarsi che l'app rimanga efficiente nel tempo. AirSHIFT ora utilizza bundlesize per non superare le dimensioni correnti dei file JavaScript e CSS. Oltre a impostare questi budget di base, ha creato una dashboard che mostra i vari percentili del tempo di caricamento della tabella delle variazioni per verificare se l'applicazione funziona anche in condizioni non ideali.

  • Il tempo di completamento dello script per ogni evento Redux viene ora misurato
  • I dati sulle prestazioni vengono raccolti in Elasticsearch
  • Il rendimento del 10°, 25°, 50° e 75° percentile di ogni evento viene visualizzato con Kibana

AirSHIFT sta ora monitorando l'evento di caricamento della tabella delle turni per assicurarsi che venga completato in 3 secondi per gli utenti del 75° percentile. Al momento questo budget non è applicato, ma sta prendendo in considerazione le notifiche automatiche tramite Elasticsearch quando il budget viene superato.

Un grafico che mostra che il 75° percentile completa in circa 2500 ms, il 50° percentile in circa 1250 ms, il 25° percentile in circa 750 ms e il 10° percentile in circa 500 ms.
La dashboard di Kibana che mostra i dati sulle prestazioni giornaliere per percentili.

Risultati

Dal grafico sopra, puoi notare che AirSHIFT sta ora raggiungendo principalmente il budget di 3 secondi per gli utenti del 75° percentile e sta anche caricando la tabella delle variazioni in un secondo per gli utenti del 25° percentile. Acquisendo i dati sulle prestazioni del RUM da varie condizioni e dispositivi, AirSHIFT ora può verificare se una nuova release di funzionalità stia effettivamente influenzando le prestazioni dell'applicazione.

5. Hackathon prestazioni

Anche se tutti questi sforzi per l'ottimizzazione delle prestazioni sono stati importanti e di grande impatto, non è sempre facile fare in modo che i team di progettazione e aziendali diano la priorità allo sviluppo non funzionale. Parte del problema è che alcune di queste ottimizzazioni delle prestazioni non possono essere pianificate. Richiedono sperimentazione e una mentalità basata su prove ed errori.

AirSHIFT sta attualmente conducendo hackathon interni relativi alle prestazioni di 1 giorno per consentire agli ingegneri di concentrarsi solo sul lavoro correlato alle prestazioni. In questi hackathon rimuovono tutti i vincoli e rispettano la creatività degli ingegneri, il che significa che vale la pena prendere in considerazione qualsiasi implementazione che contribuisca alla velocità. Per accelerare l'hackathon, AirSHIFT suddivide il gruppo in piccoli team, ognuno dei quali compete per vedere chi riesce a ottenere il maggiore miglioramento del punteggio delle prestazioni di Lighthouse. I team diventano molto competitivi! 🔥

Foto dell&#39;hackathon.

Risultati

L'approccio dell'hackathon sta funzionando bene per loro.

  • I colli di bottiglia delle prestazioni possono essere facilmente rilevati provando diversi approcci durante l'hackathon e misurandoli con Lighthouse.
  • Dopo l'hackathon, è piuttosto facile convincere il team a quale ottimizzazione dovrebbe dare la priorità per la release di produzione.
  • È anche un modo efficace per sostenere l'importanza della velocità. Ogni partecipante può comprendere la correlazione tra il modo in cui scrivi il codice e i risultati in termini di prestazioni.

Un buon effetto collaterale è stato che molti altri team di tecnici di Recruit si sono interessati a questo approccio pratico e il team di AirSHIFT sta ora organizzando diversi hackathon di velocità all'interno dell'azienda.

Riepilogo

Sicuramente non è stato il percorso più facile per AirSHIFT lavorare su queste ottimizzazioni, ma ha sicuramente dato i suoi frutti. Ora AirSHIFT sta caricando la tabella delle variazioni entro 1,5 secondi in media, un miglioramento di 6 volte delle prestazioni prima del progetto.

Dopo il lancio delle ottimizzazioni del rendimento, un utente ha dichiarato:

Grazie mille per aver caricato rapidamente la tabella dei turni. Organizzare il lavoro a turni è molto più efficiente ora.