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

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

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 adattata, in particolare per le app di produttività per computer che le persone usano ogni giorno. Il team tecnico di Recruit Technologies ha eseguito un progetto di refactoring per migliorare una delle loro app web, AirSHIFT, per migliorare le prestazioni relative all'input degli utenti. Vediamo come ci sono riusciti.

Risposta lenta, minore 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 i client, tra cui varie tabelle di griglie dei programmi dei turni organizzati per giorno, settimana, mese e altro ancora.

Uno screenshot dell'app web AirSHIFT.

Quando il team di ingegneria di Recruit Technologies ha aggiunto nuove funzionalità all'app AirSHIFT, ha iniziato a ricevere più feedback relativi a prestazioni lente. Yosuke Furukawa, Engineering Manager di AirSHIFT, ha dichiarato:

In un sondaggio di ricerca sugli utenti, siamo rimasti scioccati quando una delle proprietarie del negozio ha dichiarato di lasciare il suo posto per fare il caffè dopo aver fatto clic su un pulsante, solo per far passare il tempo in attesa del caricamento della tabella dei turni.

Dopo aver esaminato la ricerca, il team di ingegneri si è reso conto che molti utenti stavano tentando di caricare enormi tabelle di spostamento su computer con specifiche ridotte, ad esempio un laptop Celeron M da 1 GHz di 10 anni fa.

Spinner infinito su dispositivi di fascia bassa.

L'app AirSHIFT bloccava il thread principale con script costosi, ma il team tecnico non si rendeva conto di quanto fossero costosi perché stava sviluppando e testando su computer con specifiche avanzate e connessioni Wi-Fi veloci.

Un grafico che mostra l'attività di runtime dell'app.
Durante il caricamento della tabella dei turni, circa l'80% del tempo di caricamento è stato utilizzato per l'esecuzione degli script.

Dopo aver eseguito il profiling delle prestazioni in Chrome DevTools con il throttling della CPU e della rete abilitato, è emerso chiaramente che era necessaria un'ottimizzazione delle prestazioni. AirSHIFT ha formato una task force per affrontare questo problema. Ecco 5 punti su cui si è concentrato per rendere la sua app più reattiva all'input degli utenti.

1. Virtualizza tabelle di grandi dimensioni

La visualizzazione della tabella dei turni richiedeva diversi passaggi costosi: la costruzione del DOM virtuale e il rendering sullo schermo in proporzione al numero di membri del personale e fasce orarie. Ad esempio, se un ristorante ha 50 membri attivi e volesse controllare la pianificazione dei turni mensili, verrebbe creata una tabella di 50 (membri) moltiplicata per 30 (giorni) che porterebbe a 1500 componenti di cella da visualizzare. Si tratta di un'operazione molto costosa, in particolare per i dispositivi con specifiche ridotte. In realtà, le cose andavano peggio. Dalla ricerca è emerso che i negozi gestivano 200 dipendenti, il che richiedeva circa 6000 componenti di celle in un'unica tabella mensile.

Per ridurre il costo di questa operazione, AirSHIFT ha virtualizzato la tabella dei turni. Ora l'app monta solo i componenti all'interno dell'area visibile e smonta i componenti non sullo schermo.

Uno screenshot annotato che dimostra che AirSHIFT era utilizzato per eseguire il rendering dei contenuti al di fuori dell'area visibile.
Prima: viene visualizzato il rendering di tutte le celle della tabella di spostamento.
Uno screenshot annotato che dimostra che AirSHIFT ora esegue il rendering solo dei contenuti visibili nell'area visibile.
Dopo: viene visualizzato solo il rendering delle celle all'interno dell'area visibile.

In questo caso, AirSHIFT ha utilizzato react-virtualized perché erano presenti requisiti per l'attivazione di tabelle di griglie complesse in due dimensioni. Inoltre, stanno valutando la possibilità di convertire l'implementazione in modo da utilizzare in futuro la libreria react-window leggera.

Risultati

La sola virtualizzazione della tabella ha ridotto il tempo di scripting di 6 secondi (in un ambiente Macbook Pro con rallentamento della CPU 4x e 3G veloce limitato). Si tratta del miglioramento delle prestazioni più significativo del progetto di refactoring.

Uno screenshot annotato di una registrazione del riquadro Rendimento di Chrome DevTools.
Prima: circa 10 secondi di script dopo l'input dell'utente.
Un altro screenshot con annotazioni di una registrazione nel riquadro Performance 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 ristrutturato gli script eseguiti in base all'input dell'utente. Il diagramma a forma di fiamma di Chrome DevTools consente di analizzare cosa sta effettivamente accadendo nel thread principale. Tuttavia, il team di AirSHIFT ha trovato più facile analizzare l'attività dell'applicazione in base al ciclo di vita di React.

React 16 fornisce la traccia delle 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.
Gli eventi User Timing di React.

Risultati

Il team di AirSHIFT ha scoperto che un'riconciliazione dell'albero React non necessaria si verificava prima di ogni navigazione nel percorso. Ciò significava che React aggiornava la tabella di spostamento inutilmente prima delle navigazioni. Il problema era causato da un aggiornamento dello stato Redux non necessario. La correzione ha consentito di risparmiare circa 750 ms di tempo di scripting. AirSHIFT ha apportato anche altre microottimizzazioni che hanno portato a una riduzione totale del tempo di scripting di 1 secondo.

3. Carica i componenti in modo lazy e sposta la logica dispendiosa nei worker web

AirSHIFT ha un'applicazione di chat integrata. Molti proprietari di negozi comunicano con il personale tramite la chat mentre consultano la tabella dei turni, il che significa che un utente potrebbe digitare un messaggio durante il caricamento della tabella. Se il thread principale è occupato da 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 segnaposto per i contenuti delle tabelle durante il caricamento lento dei componenti effettivi.

Il team di AirSHIFT ha anche eseguito la migrazione di parte della logica di business di costo elevato all'interno dei componenti caricati a livello di latenza ai worker web. In questo modo è stato risolto il problema di balbuzie dell'input dell'utente liberando il thread principale in modo che potesse concentrarsi sulla risposta all'input dell'utente.

In genere gli sviluppatori devono affrontare complessità nell'utilizzo dei worker, ma questa volta Comlink ha fatto il lavoro più difficile per loro. Di seguito è riportato lo pseudocodice di come AirSHIFT ha automatizzato una delle operazioni più costose: il calcolo dei costi totali del lavoro.

In App.js, utilizza React.lazy e Suspense per mostrare i contenuti di riserva 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 Costo, utilizza il 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 esponilo con il 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 sottoposto a workerizzazione come prova, AirSHIFT ha spostato circa 100 ms di codice JavaScript dal thread principale al thread worker (simulato con il throttling della CPU 4 volte).

Uno screenshot di una registrazione del riquadro Rendimento di Chrome DevTools che mostra che la scrittura di script ora avviene in un web worker anziché nel thread principale.

Al momento AirSHIFT sta valutando la possibilità di caricare in modo lazy altri componenti e di scaricare più logica sui worker web per ridurre ulteriormente il jitter.

4. Impostare un budget delle prestazioni

Dopo aver implementato tutte queste ottimizzazioni, era fondamentale assicurarsi che l'app continui a funzionare nel tempo. AirSHIFT ora utilizza bundlesize per non superare le dimensioni correnti del file JavaScript e CSS. Oltre a impostare questi budget di base, hanno creato una dashboard per mostrare vari percentile del tempo di caricamento della tabella dei turni per verificare se l'applicazione è efficiente anche in condizioni non ideali.

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

AirSHIFT ora monitora l'evento di caricamento della tabella dei turni per assicurarsi che venga completato in 3 secondi per il 75° percentile degli utenti. Per il momento, questo è un budget non applicato, ma sta valutando la possibilità di ricevere notifiche automatiche tramite Elasticsearch quando superano il budget.

Un grafico che mostra che il 75° percentile viene completato 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 sul rendimento giornaliero per percentile.

Risultati

Dal grafico sopra, puoi capire che AirSHIFT ora raggiunge principalmente il budget di 3 secondi per gli utenti del 75° percentile e carica anche la tabella di spostamento entro un secondo per gli utenti del 25° percentile. Acquisendo i dati sulle prestazioni RUM da varie condizioni e dispositivi, AirSHIFT ora può verificare se il rilascio di una nuova funzionalità influisce effettivamente sulle prestazioni dell'applicazione.

5. Hackathon sul rendimento

Anche se tutti questi sforzi di ottimizzazione del rendimento sono stati importanti e significativi, non è sempre facile convincere i team di ingegneria e aziendali a dare la priorità allo sviluppo non funzionale. Parte della sfida è che alcune di queste ottimizzazioni del rendimento non possono essere pianificate. Richiedono sperimentazione e un approccio di prova ed errore.

AirSHIFT sta ora conducendo hackathon interni sulle prestazioni di 1 giorno per consentire ai tecnici di concentrarsi solo sul lavoro relativo alle prestazioni. In questi hackathon vengono rimossi tutti i vincoli e rispettata la creatività degli ingegneri, il che significa che qualsiasi implementazione che contribuisca alla velocità vale la pena di essere presa in considerazione. Per accelerare l'hackathon, AirSHIFT suddivide il gruppo in piccoli team e ogni team compete per ottenere il maggiore miglioramento del punteggio di rendimento di Lighthouse. Le squadre diventano molto competitive. 🔥

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 più approcci durante l'hackathon e misurando ciascuno con Lighthouse.
  • Dopo l'hackathon, è piuttosto facile convincere il team a dare la priorità all'ottimizzazione per il rilascio in produzione.
  • Inoltre, si tratta di un modo efficace per promuovere l'importanza della velocità. Ogni partecipante può comprendere la correlazione tra il modo in cui scrivi il codice e il rendimento che ne deriva.

Un buon effetto collaterale è stato che molti altri team di ingegneria di Recruit si sono interessati a questo approccio pratico e il team di AirSHIFT ora organizza più speed hackathon all'interno dell'azienda.

Riepilogo

Non è stato sicuramente il percorso più semplice per AirSHIFT lavorare a queste ottimizzazioni, ma ha sicuramente dato i suoi frutti. Ora AirSHIFT carica la tabella dei turni in media in 1,5 secondi, un miglioramento di sei volte rispetto alle prestazioni pre-progetto.

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

Grazie mille per aver velocizzato il caricamento della tabella dei turni. Organizzare il lavoro dei turni ora è molto più efficiente.