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

Eine reale Fallstudie zur Leistungsoptimierung von React SPA.

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

Bei der Websiteleistung geht es nicht nur um die Ladezeit. Es ist entscheidend, eine schnelle und reaktionsschnelle Nutzererfahrung zu bieten, insbesondere bei Produktivitäts-Desktop-Apps, die täglich von Nutzern verwendet werden. Das Engineering-Team von Recruit Technologies hat ein Refactoring-Projekt durchgeführt, um eine seiner Webanwendungen, AirSHIFT, zu verbessern, um die Eingabeleistung der Nutzer zu verbessern. Und so haben sie vorgegangen:

Lange Antwortzeit, geringere Produktivität

AirSHIFT ist eine Desktop-Webanwendung, mit der Geschäftsinhaber wie Restaurants und Cafés die Schicht ihrer Mitarbeiter verwalten können. Die in React eingebundene Single-Page-Anwendung bietet vielfältige Clientfunktionen, darunter verschiedene Rastertabellen mit Schichtplänen, die nach Tag, Woche, Monat usw. geordnet sind.

Screenshot der AirSHIFT-Web-App

Als das Engineering-Team von Recruit Technologies der AirSHIFT-App neue Funktionen hinzufügte, erhielt es vermehrtes Feedback zu Leistungseinbußen. Der Engineering Manager von AirSHIFT, Yosuke Furukawa, sagte:

In einer Nutzerstudie waren wir schockiert, als eine der Ladenbesitzerinnen sagte, dass sie nach dem Klicken auf eine Schaltfläche ihren Platz verlassen würde, um Kaffee zu kochen, nur um die Zeit mit dem Warten auf das Laden der Schichtfläche zu verschwenden.

Nach der Recherche stellte das Engineering-Team fest, dass viele Nutzer versuchten, riesige Schalttabellen auf Low-Spec-Computern zu laden, zum Beispiel auf einem Celeron M-Laptop mit 1 GHz von vor 10 Jahren.

Rotierendes Endlossymbol auf Low-End-Geräten.

Die AirSHIFT-App blockierte den Hauptthread mit teuren Skripts, aber das Entwicklerteam wusste nicht, wie teuer die Skripts waren, da sie auf umfassenden Computern mit 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 Skripts verbraucht.

Nachdem sie in den Chrome-Entwicklertools mit aktivierter CPU- und Netzwerkdrosselung ein Leistungsprofil erstellt hatten, wurde klar, dass die Leistung optimiert werden muss. AirSHIFT hat eine Taskforce gebildet, die sich mit diesem Problem befasst. Hier sind fünf Dinge, auf die sie sich konzentrieren, um ihre App besser auf Nutzereingaben zu reagieren.

1. Große Tabellen virtualisieren

Das Anzeigen der Schichttabelle erforderte mehrere kostspielige Schritte: das Erstellen des virtuellen DOMs und das Rendern auf dem Bildschirm im Verhältnis zur Anzahl der Mitarbeiter und zu den Zeitblöcken. Wenn ein Restaurant beispielsweise 50 berufstätige Mitglieder hat und seinen monatlichen Schichtplan überprüfen möchte, würde dies eine Tabelle von 50 (Mitgliedern) multipliziert mit 30 (Tage) ergeben, sodass 1.500 Zellenkomponenten gerendert werden. Dieser Vorgang ist sehr kostspielig, insbesondere bei Geräten mit wenigen Spezifikationen. In Wirklichkeit war die Situation sogar noch schlimmer. Aus der Forschung geht hervor, dass es Geschäfte gibt,die 200 Mitarbeitende verwalten und etwa 6.000 Zellenkomponenten in einer einzigen monatlichen Tabelle benötigen.

Um die Kosten dieses Vorgangs zu reduzieren, hat AirSHIFT die Schichttabelle virtualisiert. Die App stellt die Komponenten jetzt nur noch im Darstellungsbereich bereit und trennt die nicht sichtbaren Komponenten.

Ein Screenshot mit Anmerkungen, auf dem zu sehen ist, dass mit AirSHIFT Inhalte außerhalb des Darstellungsbereichs gerendert wurden.
Vorher: Rendering aller Zellen der Verschiebetabelle.
Ein Screenshot mit Anmerkungen, der zeigt, dass AirSHIFT jetzt nur Inhalte rendert, die im Darstellungsbereich sichtbar sind.
Nachher: Nur die Zellen im Darstellungsbereich werden gerendert.

In diesem Fall nutzte AirSHIFT react-virtualisierte, weil komplexe zweidimensionale Rastertabellen aktiviert werden mussten. Außerdem wird geprüft, wie sich die Implementierung künftig auf das einfache React-Window umstellen lässt.

Ergebnisse

Allein durch die Visualisierung der Tabelle konnte die Skriptzeit um 6 Sekunden reduziert werden (bei einer 4-fachen CPU-Verlangsamung und einer schnellen 3G-gedrosselten Macbook Pro-Umgebung). Dies war die wirkungsvollste Leistungsverbesserung im Refaktorierungsprojekt.

Screenshot einer Aufzeichnung im Bereich „Leistung“ der Chrome-Entwicklertools mit Anmerkungen
Vorher: Etwa 10 Sekunden nach der Nutzereingabe.
Ein weiterer kommentierter Screenshot einer Aufzeichnung im Bereich „Leistung“ der Chrome-Entwicklertools.
Nach: 4 Sekunden Skripterstellung nach Nutzereingabe.

2. Audit mit der User Timing API

Als Nächstes überarbeitete das AirSHIFT-Team die Skripts, die bei Nutzereingaben ausgeführt werden. Mit dem Flame-Diagramm der Chrome-Entwicklertools kann analysiert werden, was im Hauptthread tatsächlich passiert. Das AirSHIFT-Team fand es jedoch einfacher, die Anwendungsaktivität basierend auf dem Lebenszyklus von React zu analysieren.

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

Der Bereich „Timings“ im Bereich „Leistung“ der Chrome-Entwicklertools.
Nutzertiming-Ereignisse von React.

Ergebnisse

Das AirSHIFT-Team stellte fest, dass unmittelbar vor jeder Routenführung ein unnötiger React Tree-Abgleich durchgeführt wurde. Dies bedeutete, dass React die Schichttabelle vor den Navigationen unnötig aktualisiert hatte. Das Problem wurde durch eine unnötige Redux-Statusaktualisierung verursacht. Durch die Behebung des Problems konnten ca. 750 ms Skriptzeit eingespart werden. Außerdem führte AirSHIFT weitere Mikrooptimierungen durch, die letztendlich zu einer Gesamtverkürzung der Skriptzeit um 1 Sekunde führten.

3. Verzögern Sie das Laden von Komponenten und verschieben Sie teure Logik auf Web-Worker

AirSHIFT verfügt über eine integrierte Chatanwendung. Viele Ladenbesitzer kommunizieren über den Chat mit ihren Mitarbeitern, während sie sich den Schichttisch ansehen. Das bedeutet, dass ein Nutzer möglicherweise eine Nachricht eingibt, während die Tabelle geladen wird. Wenn der Hauptthread mit Skripts belegt ist, die die Tabelle rendern, kann die Nutzereingabe verzögert sein.

AirSHIFT verwendet jetzt React.lazy und Suspense, um Platzhalter für Tabelleninhalte einzublenden, während die eigentlichen Komponenten nach dem Lazy Loading geladen werden.

Das AirSHIFT-Team migrierte auch einen Teil der teuren Geschäftslogik innerhalb der langsam geladenen Komponenten zu Web-Workern. Dadurch wurde das Problem mit Verzögerungen bei der Nutzereingabe gelöst, da der Hauptthread freigegeben wurde, sodass er sich auf die Reaktion auf Nutzereingaben konzentrieren konnte.

Entwickler sehen sich in der Regel mit einer komplexen Verwendung von Workern konfrontiert, aber dieses Mal hat Comlink den Großteil der Arbeit für sie erledigt. Unten sehen Sie den Pseudocode, der zeigt, wie AirSHIFT einen der teuersten Vorgänge des Unternehmens durchgeführt hat: die Berechnung der Gesamtarbeitskosten.

In App.js mithilfe von React.lazy und Suspense Fallback-Inhalte beim Laden 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>
  )
}

Verwenden Sie in der Kostenkomponente „comlink“, um die Berechnungslogik auszufü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>;
}

Die im Worker ausgeführte Berechnungslogik implementieren und mit comlink verfügbar machen

// 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 der begrenzten Menge an Logik, die im Test verwendet wurde, verlagerte AirSHIFT etwa 100 ms des JavaScript-Codes vom Hauptthread in den Worker-Thread (simuliert mit einer 4‐fachen CPU-Drosselung).

Screenshot eines Bereichs „Leistung“ in den Chrome-Entwicklertools, auf dem zu sehen ist, dass Scripting jetzt auf einem Web Worker und nicht im Hauptthread ausgeführt wird.

AirSHIFT prüft derzeit, ob andere Komponenten per Lazy Loading geladen und mehr Logik auf Web-Worker übertragen werden kann, um Verzögerungen weiter zu reduzieren.

4. Leistungsbudget festlegen

Nach der Implementierung all dieser Optimierungen war es wichtig, die Leistung der Anwendung langfristig zu sichern. 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, das verschiedene Perzentile der Ladezeit der Schichttabelle anzeigt, um zu prüfen, ob die Anwendung auch unter nicht idealen Bedingungen leistungsfähig ist.

  • Die Zeit bis zur Skriptausführung 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 Laden der Shift-Tabelle, um sicherzustellen, dass es für Nutzer des 75. Perzentils in 3 Sekunden abgeschlossen wird. Dies ist derzeit ein nicht erzwungenes Budget, aber es zieht automatische Benachrichtigungen über Elasticsearch in Betracht, wenn das Budget überschritten wird.

Ein Diagramm, das zeigt, dass das 75. Perzentil in etwa 2.500 ms, das 50. Perzentil in etwa 1.250 ms, das 25. Perzentil in etwa 750 ms und das 10. Perzentil in etwa 500 ms abgeschlossen ist.
Das Kibana-Dashboard mit täglichen Leistungsdaten nach Perzentilen.

Ergebnisse

Anhand des obigen Diagramms können Sie sehen, dass AirSHIFT nun das 3-Sekunden-Budget für Nutzer des 75. Perzentils fast erreicht und auch die Schichttabelle für Nutzer des 25. Perzentils innerhalb einer Sekunde lädt. Durch die Erfassung von RUM-Leistungsdaten von verschiedenen Bedingungen und Geräten kann AirSHIFT jetzt prüfen, ob eine neue Funktionsversion tatsächlich die Leistung der Anwendung beeinträchtigt oder nicht.

5. Performance-Hackathons

Obwohl alle diese Maßnahmen zur Leistungsoptimierung wichtig und wirkungsvoll waren, ist es nicht immer einfach, Entwickler- und Geschäftsteams dazu zu bringen, nicht funktionale Entwicklungen zu priorisieren. Ein Teil der Herausforderung besteht darin, dass einige dieser Leistungsoptimierungen nicht geplant werden können. Sie erfordern Experimente und eine Denkweise von Versuch und Irrtum.

AirSHIFT führt jetzt interne eintägige Performance-Hackathons durch, damit sich die Entwickler auf ihre Arbeit konzentrieren können. Bei diesen Hackathons beseitigen sie alle Einschränkungen und respektieren die Kreativität der Engineers. Jede Implementierung, die zur Geschwindigkeit beiträgt, ist also eine Überlegung wert. Um den Hackathon zu beschleunigen, teilt AirSHIFT die Gruppe in kleine Teams auf, die jeweils gegeneinander antreten, um herauszufinden, wer die größte Verbesserung der Lighthouse-Leistung erzielen kann. Die Teams werden sehr wettbewerbsorientiert! 🔥

Fotos vom Hackathon

Ergebnisse

Der Hackathon-Ansatz funktioniert gut für sie.

  • Leistungsengpässe lassen sich leicht erkennen, wenn Sie während des Hackathons mehrere Ansätze ausprobieren und jeden mit Lighthouse messen.
  • Nach dem Hackathon ist es ziemlich einfach, das Team davon zu überzeugen, welche Optimierung es bei der Produktionsfreigabe priorisieren sollte.
  • Es ist auch ein effektives Mittel, um die Bedeutung von Geschwindigkeit zu betonen. Alle Teilnehmenden können den Zusammenhang zwischen der Programmierung und den Ergebnissen ihrer Leistung nachvollziehen.

Ein positiver Nebeneffekt war, dass sich viele andere Entwicklungsteams bei Recruit für diesen praktischen Ansatz interessiert haben. Das AirSHIFT-Team organisiert jetzt mehrere Speed-Hackathons innerhalb des Unternehmens.

Zusammenfassung

Für AirSHIFT war es definitiv nicht die leichteste Methode, an diesen Optimierungen zu arbeiten, aber es hat sich auf jeden Fall ausgezahlt. Jetzt lädt AirSHIFT die Schichttabelle innerhalb von 1,5 Sekunden im Medianwert, was eine 6-fache Verbesserung der Leistung vor dem Projekt ist.

Nach der Einführung der Leistungsoptimierung sagte ein Nutzer:

Vielen Dank, dass Sie dafür gesorgt haben, dass die Schichttabelle schnell geladen wird. Schichtarbeiten lassen sich jetzt viel effizienter organisieren.