Fünf Möglichkeiten, wie AirSHIFT die Laufzeitleistung der React-App verbessert hat

Eine Fallstudie aus der Praxis zur Leistungsoptimierung von React SPA.

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

Die Websiteleistung hängt nicht nur von der Ladezeit ab. Es ist wichtig, Nutzern eine schnelle und reaktionsschnelle Nutzung zu ermöglichen, insbesondere bei Desktop-Produktivitäts-Apps, die Nutzer täglich verwenden. Das Entwicklerteam von Recruit Technologies führte ein Refactoring-Projekt durch, um eine seiner Web-Apps, AirSHIFT, zu verbessern, um die Leistung von Nutzereingaben zu verbessern. So gings:

Langsame Reaktion, geringere Produktivität

AirSHIFT ist eine Desktop-Webanwendung, mit der Geschäftsinhaber wie Restaurants und Cafés die Schichtarbeit ihrer Mitarbeiter verwalten können. Die Single-Page-Anwendung wurde mit React erstellt und bietet umfangreiche Clientfunktionen, darunter verschiedene Tabellen mit Schichtplänen, die nach Tag, Woche, Monat usw. organisiert sind.

Screenshot der AirSHIFT-Web-App

Als das Engineering-Team von Recruit Technologies der AirSHIFT App neue Funktionen hinzufügte, erhielt es mehr Feedback zur langsamen Leistung. Der Engineering Manager von AirSHIFT, Yosuke Furukawa, sagte:

Bei einer Nutzerstudie waren wir schockiert, als eine der Geschäftsinhaberinnen sagte, dass sie ihren Platz verlässt, um Kaffee zu kochen, nachdem sie auf eine Schaltfläche geklickt hat, nur um die Zeit zu vertreiben, während sie auf das Laden der Schichttabelle wartet.

Nach der Recherche stellte das Engineering-Team fest, dass viele Nutzer versuchten, riesige Schichtpläne auf Computern mit niedrigen Spezifikationen zu laden, z. B. auf einem 1-GHz-Celeron-M-Laptop von vor zehn Jahren.

Endloses Kreiselsymbol auf Low-End-Geräten.

Die AirSHIFT-Anwendung blockierte den Hauptthread mit teuren Skripts. Das Entwicklerteam war sich jedoch nicht bewusst, wie teuer die Skripts waren, da sie auf Computern mit leistungsfähiger Spezifikation und schnellen WLAN-Verbindungen entwickelt und getestet wurden.

Ein Diagramm, das die Laufzeitaktivität der App zeigt.
Beim Laden der Schichttabelle wurden etwa 80 % der Ladezeit durch das Ausführen von Scripts in Anspruch genommen.

Nachdem die Leistung in den Chrome DevTools mit aktivierter CPU- und Netzwerkdrosselung analysiert wurde, wurde klar, dass eine Leistungsoptimierung erforderlich war. AirSHIFT hat eine Taskforce gegründet, die dieses Problem angehen soll. Hier sind fünf Dinge, auf die sich das Team konzentriert hat, um die App reaktionsschneller auf Nutzereingaben zu machen.

1. Große Tabellen virtualisieren

Die Anzeige der Dienstplantabelle erforderte mehrere aufwendige Schritte: das Erstellen des virtuellen DOM und das Rendern auf dem Bildschirm in Abhängigkeit von der Anzahl der Mitarbeiter und Zeitblöcke. Wenn ein Restaurant beispielsweise 50 Mitarbeiter hat und seinen monatlichen Schichtplan prüfen möchte, würde das eine Tabelle mit 50 Mitarbeitern multipliziert mit 30 Tagen ergeben, was zu 1.500 Zellenkomponenten führen würde, die gerendert werden müssen. Das ist ein sehr teurer Vorgang, insbesondere bei Geräten mit niedrigen Spezifikationen. In Wirklichkeit war es schlimmer. Die Studie ergab, dass es Geschäfte mit 200 Mitarbeitern gab,die etwa 6.000 Zellenkomponenten in einer einzigen monatlichen Tabelle benötigen.

Um die Kosten für diesen Vorgang zu senken, hat AirSHIFT die Schichttabelle virtualisiert. Die App montiert jetzt nur die Komponenten innerhalb des Darstellungsbereichs und demontiert die Komponenten außerhalb des Bildschirms.

Ein Screenshot mit Anmerkungen, der zeigt, dass AirSHIFT Inhalte außerhalb des Darstellungsbereichs gerendert hat.
Vorher: Alle Zellen der Tabellenüberblendung werden gerendert.
Ein Screenshot mit Anmerkungen, der zeigt, dass AirSHIFT jetzt nur Inhalte rendert, die im Darstellungsbereich sichtbar sind.
Nachher: Es werden nur die Zellen im Darstellungsbereich gerendert.

In diesem Fall verwendete AirSHIFT reagiert virtualisiert, da es Anforderungen in Bezug auf die Aktivierung komplexer zweidimensionaler Rastertabellen gab. Außerdem wird untersucht, wie die Implementierung in Zukunft auf das schlanke react-window umgestellt werden kann.

Ergebnisse

Durch die Virtualisierung der Tabelle allein konnte die Skriptzeit um 6 Sekunden reduziert werden (bei einer 4-mal schnelleren CPU-Verlangsamung und einer gedrosselten MacBook Pro-Umgebung mit schnellem 3G). Dies war die leistungsstärkste Verbesserung im Rahmen des Refactoring-Projekts.

Ein Screenshot mit Anmerkungen einer Aufzeichnung des Chrome DevTools-Steuerfelds „Leistung“.
Vorher: Ungefähr 10 Sekunden Skripterstellung nach Nutzereingabe.
Ein weiterer mit Anmerkungen versehener Screenshot einer Aufzeichnung im Bereich „Leistung“ in den Chrome-Entwicklertools.
Nach: 4 Sekunden Scripting nach Nutzereingabe.

2. Prüfung mit User Timing API

Als Nächstes überarbeitete das AirSHIFT-Team die Skripts, die bei Nutzereingaben ausgeführt werden. Mit dem Feuerdiagramm der Chrome DevTools können Sie analysieren, was im Hauptthread tatsächlich passiert. Das AirSHIFT-Team fand es jedoch einfacher, die Anwendungsaktivitäten basierend auf dem Lebenszyklus von React zu analysieren.

React 16 stellt seine Leistungsaufzeichnung über die User Timing API bereit. Sie können sie im Abschnitt „Timings“ der Chrome-Entwicklertools visualisieren. AirSHIFT nutzte den Bereich „Timings“, um unnötige Logik zu finden, die in React-Lebenszyklusereignissen ausgeführt wird.

Im Bereich „Leistung“ der Chrome-Entwicklertools im Abschnitt „Zeiten“.
Nutzerzeit-Ereignisse von React.

Ergebnisse

Das AirSHIFT-Team stellte fest, dass direkt vor jeder Routennavigation eine unnötige React-Baumübereinstimmung durchgeführt wurde. Das bedeutete, dass React die Schichttabelle vor Navigationen unnötig aktualisierte. Dieses Problem wurde durch ein unnötiges Redux-Status-Update verursacht. Die Behebung des Fehlers spart etwa 750 ms Skriptzeit. AirSHIFT führte auch weitere Mikrooptimierungen durch, die schließlich zu einer Reduzierung der gesamten Skriptzeit von einer Sekunde führten.

3. Komponenten mit verzögertem Laden und teure Logik in Webworker verschieben

AirSHIFT hat eine integrierte Chat-Anwendung. Viele Geschäftsinhaber kommunizieren über den Chat mit ihren Mitarbeitern, während sie sich die Dienstplantabelle ansehen. Das bedeutet, dass ein Nutzer möglicherweise eine Nachricht tippt, während die Tabelle geladen wird. Wenn der Haupt-Thread mit Scripts belegt ist, die die Tabelle rendern, kann die Nutzereingabe ruckelig sein.

Um die Nutzerfreundlichkeit zu verbessern, verwendet AirSHIFT jetzt React.lazy und Suspense, um Platzhalter für den Tabelleninhalt anzuzeigen und die eigentlichen Komponenten zeitverzögert zu laden.

Das AirSHIFT-Team migrierte außerdem einen Teil der ressourcenintensiven Geschäftslogik in den verzögert geladenen Komponenten zu Webworkern. Dadurch wurde das Problem mit der Ruckler bei der Nutzereingabe behoben, da der Hauptthread freigegeben wurde, damit er sich auf die Reaktion auf Nutzereingaben konzentrieren konnte.

Normalerweise sind die Verwendung von Workern für Entwickler sehr komplex. Dieses Mal hat Comlink die ganze Arbeit für sie erledigt. Unten sehen Sie den Pseudocode, mit dem AirSHIFT einen der teuersten Vorgänge automatisiert hat: die Berechnung der Gesamtarbeitskosten.

In App.js mit React.lazy und Suspense Fallback-Inhalte während des Ladevorgangs anzeigen

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

In der Kostenkomponente mit comlink die Berechnungslogik ausführen

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

Berechnungslogik im Worker implementieren und mit comlink freigeben

// 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);

Ergebnisse

Trotz des begrenzten Umfangs an Logik, den sie als Testversion ausführten, verlagerte AirSHIFT etwa 100 ms des JavaScript-Codes vom Hauptthread zum Worker-Thread (simuliert mit einer 4-fachen CPU-Drosselung).

Screenshot einer Aufzeichnung im Chrome DevTools-Steuerfeld „Leistung“, aus dem hervorgeht, dass das Scripting jetzt in einem Webworker statt im Hauptthread ausgeführt wird

AirSHIFT prüft derzeit, ob Lazy Loading für andere Komponenten und die Auslagerung weiterer Logik an Web-Worker zur weiteren Reduzierung der Verzögerung möglich ist.

4. Leistungsbudget festlegen

Nachdem wir all diese Optimierungen implementiert hatten, war es wichtig, dafür zu sorgen, dass die App auch langfristig leistungsfähig bleibt. AirSHIFT verwendet jetzt bundlesize, um die aktuelle JavaScript- und CSS-Dateigröße nicht zu überschreiten. Neben der Festlegung dieser grundlegenden Budgets wurde ein Dashboard erstellt, um verschiedene Perzentile der Ladezeit der Schichttabelle anzuzeigen, um zu prüfen, ob die Anwendung auch unter nicht idealen Bedingungen leistungsfähig ist.

  • Die Ausführungszeit des Scripts für jedes Redux-Ereignis wird jetzt gemessen.
  • Leistungsdaten werden in Elasticsearch erfasst
  • Die Leistung des 10., 25., 50. und 75. Perzentils jedes Ereignisses wird mit Kibana visualisiert.

AirSHIFT überwacht jetzt das Ladeereignis der Schichttabelle, um sicherzustellen, dass es für Nutzer des 75. Perzentils in 3 Sekunden abgeschlossen ist. Dieses Budget wird vorerst nicht erzwungen, erwägt jedoch, automatische Benachrichtigungen über Elasticsearch zu erhalten, wenn sein Budget überschritten wird.

Ein Diagramm, das zeigt, dass der 75. Perzentilwert in etwa 2.500 ms, der 50. Perzentilwert in etwa 1.250 ms, der 25. Perzentilwert in etwa 750 ms und der 10. Perzentilwert in etwa 500 ms erreicht wird.
Das Kibana-Dashboard mit täglichen Leistungsdaten nach Prozentilen.

Ergebnisse

In der Grafik oben können Sie sehen, dass AirSHIFT jetzt größtenteils das 3-Sekunden-Budget für Nutzer des 75. Perzentils erreicht und auch die Schichttabelle für Nutzer des 25. Perzentils innerhalb einer Sekunde lädt. Durch die Erfassung von RUM-Leistungsdaten unter verschiedenen Bedingungen und auf verschiedenen Geräten kann AirSHIFT jetzt prüfen, ob sich eine neue Funktionsversion tatsächlich auf die Leistung der Anwendung auswirkt.

5. Hackathons zur Leistungssteigerung

Obwohl alle diese Bemühungen zur Leistungsoptimierung wichtig und wirkungsvoll waren, ist es nicht immer einfach, die Entwicklungs- und Geschäftsteams dazu zu bringen, nicht funktionale Entwicklungen zu priorisieren. Eine Herausforderung besteht darin, dass einige dieser Leistungsoptimierungen nicht geplant werden können. Sie erfordern Experimente und eine „Trial-and-Error“-Mentalität.

AirSHIFT führt jetzt interne eintägige Leistungs-Hackathons durch, damit sich die Entwickler nur auf leistungsbezogene Arbeit konzentrieren können. Bei diesen Hackathons beseitigen sie alle Einschränkungen und respektieren die Kreativität der Entwickler. Das bedeutet, dass jede Implementierung, die zur Geschwindigkeit führt, eine Überlegung wert ist. Um den Hackathon zu beschleunigen, teilt AirSHIFT die Gruppe in kleine Teams auf. Jedes Team tritt gegeneinander an, um herauszufinden, wer die größte Verbesserung der Lighthouse-Leistung erzielen kann. Die Teams sind sehr ehrgeizig! 🔥

Fotos vom Hackathon

Ergebnisse

Der Hackathon-Ansatz funktioniert gut für sie.

  • Leistungsengpässe lassen sich ganz einfach erkennen, indem Sie während des Hackathons mehrere Ansätze ausprobieren und mit Lighthouse messen.
  • Nach dem Hackathon ist es ziemlich einfach, das Team davon zu überzeugen, welche Optimierung für die Produktionsversion priorisiert werden sollte.
  • Es ist auch eine effektive Möglichkeit, die Bedeutung von Geschwindigkeit zu betonen. Alle Teilnehmenden verstehen den Zusammenhang zwischen der Art und Weise, wie Sie programmieren, und den Ergebnissen, die sich daraus ableiten lassen.

Ein positiver Nebeneffekt war, dass viele andere Engineering-Teams innerhalb von Recruit an diesem praxisnahen Ansatz interessiert waren. Das AirSHIFT-Team führt jetzt mehrere Speed-Hackathons im Unternehmen durch.

Zusammenfassung

Es war definitiv nicht der einfachste Weg für AirSHIFT, an diesen Optimierungen zu arbeiten, aber es hat sich definitiv gelohnt. Jetzt lädt AirSHIFT die Schichttabelle innerhalb von 1,5 Sekunden im Medianwert, was eine 6-fache Verbesserung gegenüber der Leistung vor dem Projekt ist.

Nach der Einführung der Leistungsoptimierungen sagte ein Nutzer:

Vielen Dank, dass Sie dafür gesorgt haben, dass die Schichttabelle schnell geladen wird. Die Planung der Schichtarbeit ist jetzt viel effizienter.