radEventListener: eine Geschichte über die Leistung von Client-seitigen Frameworks

Avatar of Jeremy Wagner
Jeremy Wagner am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

React ist beliebt, beliebt genug, um eine faire Portion Kritik zu erhalten. Dennoch ist diese Kritik an React nicht völlig unbegründet: React und ReactDOM ergeben zusammen etwa 120 KiB minifizierten JavaScripts, was definitiv zu einer langsamen Startzeit beiträgt. Wenn man sich vollständig auf das Client-seitige Rendering in React verlässt, ruckelt es. Selbst wenn Sie Komponenten auf dem Server rendern und sie auf dem Client hydrieren, ruckelt es immer noch, da die Komponenten-Hydration rechenintensiv ist.

React hat zweifellos seinen Platz, wenn es um Anwendungen geht, die ein komplexes Zustandsmanagement erfordern, aber meiner Berufserfahrung nach gehört es in den meisten Szenarien, in denen ich es im Einsatz sehe, nicht hin. Wenn selbst ein bisschen React auf langsamen und schnellen Geräten gleichermaßen problematisch sein kann, ist die Verwendung eine bewusste Entscheidung, die Menschen mit Low-End-Hardware effektiv ausschließt.

Wenn es so klingt, als hätte ich einen Groll gegen React, dann muss ich gestehen, dass ich sein Komponentensierungsmodell wirklich mag. Es macht die Organisation von Code einfacher. Ich halte JSX für großartig. Server-Rendering ist auch cool – auch wenn das nur die Art und Weise ist, wie wir heutzutage „HTML über das Netzwerk senden“ sagen.

Dennoch, auch wenn ich gerne React-Komponenten auf dem Server verwende (oder Preact, wie ich es bevorzuge), ist es eine Herausforderung herauszufinden, wann es angebracht ist, sie auf dem Client zu verwenden. Im Folgenden finden Sie meine Erkenntnisse zur React-Leistung, während ich versucht habe, diese Herausforderung auf eine Weise zu meistern, die für die Benutzer am besten ist.

Die Szene setzen

In letzter Zeit habe ich an einem RSS-Feed-App-Nebenprojekt namens bylines.fyi gearbeitet. Diese App verwendet JavaScript sowohl im Back- als auch im Front-End. Ich glaube nicht, dass Client-seitige Frameworks abscheuliche Dinge sind, aber ich habe in meinen täglichen Arbeiten und Recherchen häufig zwei Dinge über die Implementierungen von Client-seitigen Frameworks beobachtet

  1. Frameworks haben das Potenzial, ein tieferes Verständnis der Dinge, die sie abstrahieren, nämlich der Webplattform, zu behindern. Ohne zumindest einige der Low-Level-APIs zu kennen, auf die Frameworks angewiesen sind, können wir nicht wissen, welche Projekte von einem Framework profitieren und welche Projekte besser ohne auskommen.
  2. Frameworks bieten nicht immer einen klaren Weg zu guten Benutzererlebnissen.

Sie mögen argumentieren können, dass mein erster Punkt gültig ist, aber der zweite Punkt wird immer schwieriger zu widerlegen. Sie erinnern sich vielleicht, dass Tim Kadlec vor einiger Zeit einige Nachforschungen über HTTPArchive zur Leistung von Web-Frameworks durchgeführt hat und zu dem Schluss kam, dass React keine herausragende Leistung bietet.

Dennoch wollte ich sehen, ob es möglich wäre, das, was ich für das Beste an React hielt, auf dem Server zu nutzen und gleichzeitig seine negativen Auswirkungen auf dem Client zu mildern. Für mich macht es Sinn, gleichzeitig ein Framework nutzen zu wollen, um meinen Code zu organisieren, aber auch dessen negativen Einfluss auf das Benutzererlebnis einzuschränken. Das erforderte ein wenig Experimentieren, um zu sehen, welcher Ansatz für meine App am besten geeignet wäre.

Das Experiment

Ich sorge dafür, jede von mir verwendete Komponente auf dem Server zu rendern, da ich glaube, dass die Last der Bereitstellung von Markup vom Server der Web-App getragen werden sollte, nicht vom Gerät des Benutzers. Ich brauchte jedoch *etwas* JavaScript in meiner RSS-Feed-App, um eine umschaltbare mobile Navigation zum Laufen zu bringen.

The mobile nav toggle functionality. At left, the mobile nav is in the closed state. On the right, it’s in the open state, which overlays the entire screen with the navigation.

Dieses Szenario beschreibt treffend, was ich als *einfachen Zustand* bezeichne. Meiner Erfahrung nach sind lineare A-zu-B-Interaktionen ein Paradebeispiel für einfachen Zustand. Wir schalten eine Sache ein und dann wieder aus. Stateful, aber *einfach*.

Leider sehe ich oft stateful React-Komponenten zur Verwaltung von einfachem Zustand verwendet, was ein Kompromiss ist, der problematisch für die Leistung ist. Obwohl das im Moment eine vage Aussage sein mag, werden Sie im weiteren Verlauf feststellen. Das gesagt, ist es wichtig zu betonen, dass dies ein triviales Beispiel ist, aber es ist auch ein Kanarienvogel. Die meisten Entwickler werden – das *hoffe* ich – sich nicht allein auf React verlassen, um ein solches einfaches Verhalten für nur eine Sache auf ihrer Website zu steuern. Daher ist es wichtig zu verstehen, dass die Ergebnisse, die Sie sehen werden, dazu dienen sollen, Sie darüber zu informieren, *wie* Sie Ihre Anwendungen architekturieren, und wie sich die Auswirkungen Ihrer Framework-Entscheidungen auf die Laufzeitleistung auswirken könnten.

Die Bedingungen

Meine RSS-Feed-App befindet sich noch in der Entwicklung. Sie enthält keinen Fremdcode, was das Testen in einer ruhigen Umgebung erleichtert. Das durchgeführte Experiment verglich das Verhalten des mobilen Navigations-Toggles über drei Implementierungen hinweg

  1. Eine stateful React-Komponente (React.Component), die auf dem Server gerendert und auf dem Client hydriert wurde.
  2. Eine stateful Preact-Komponente, ebenfalls serverseitig gerendert und auf dem Client hydriert.
  3. Eine serverseitig gerenderte, stateless Preact-Komponente, die nicht hydriert wurde. Stattdessen sorgen reguläre Event-Listener auf dem Client für die Funktionalität der mobilen Navigation.

Jedes dieser Szenarien wurde in vier verschiedenen Umgebungen gemessen

  1. Ein Nokia 2 Android-Telefon mit Chrome 83.
  2. Ein ASUS X550CC Laptop von 2013 mit Windows 10 und Chrome 83.
  3. Ein altes iPhone SE erster Generation mit Safari 13.
  4. Ein neues iPhone SE zweiter Generation, ebenfalls mit Safari 13.

Ich glaube, dass diese Bandbreite an mobiler Hardware die Leistung über ein breites Spektrum von Gerätefähigkeiten hinweg veranschaulichen wird, auch wenn sie auf der Apple-Seite etwas schwer ist.

Was wurde gemessen

Ich wollte vier Dinge für jede Implementierung in jeder Umgebung messen

  1. Startzeit. Für React und Preact umfasste dies die Zeit, die zum Laden des Framework-Codes sowie zur Hydration der Komponente auf dem Client benötigt wurde. Für das Event-Listener-Szenario umfasste dies nur den Event-Listener-Code selbst.
  2. Hydrationszeit. Für die React- und Preact-Szenarien ist dies eine Teilmenge der Startzeit. Aufgrund von Problemen mit dem Absturz des Remote-Debuggings in Safari unter macOS konnte ich die Hydrationszeit auf iOS-Geräten nicht allein messen. Event-Listener-Implementierungen hatten keine Hydrationskosten.
  3. Zeit zum Öffnen der mobilen Navigation. Dies gibt uns Aufschluss darüber, wie viel Overhead Frameworks bei der Abstraktion von Event-Handlern einführen und wie sich das im Vergleich zum frameworklosen Ansatz verhält.
  4. Zeit zum Schließen der mobilen Navigation. Wie sich herausstellte, war dies deutlich weniger als die Kosten für das Öffnen des Menüs. Ich habe mich letztendlich entschieden, diese Zahlen nicht in diesem Artikel zu veröffentlichen.

Es sollte beachtet werden, dass die Messungen dieser Verhaltensweisen *nur* Skripting-Zeit beinhalten. Jegliche Layout-, Paint- und Compositing-Kosten wären zusätzlich zu diesen Messungen und außerhalb dieser. Man sollte darauf achten, sich daran zu erinnern, dass diese Aktivitäten um die Haupt-Thread-Zeit konkurrieren, zusammen mit den Skripten, die sie auslösen.

Das Verfahren

Um jede der drei Implementierungen der mobilen Navigation auf jedem Gerät zu testen, habe ich dieses Verfahren befolgt

  1. Ich habe Remote-Debugging in Chrome unter macOS für das Nokia 2 verwendet. Für iPhones habe ich das Äquivalent von Safari zum Remote-Debugging verwendet.
  2. Ich habe die RSS-Feed-App, die in meinem lokalen Netzwerk läuft, auf jedem Gerät zu derselben Seite aufgerufen, auf der der Code zum Umschalten der mobilen Navigation ausgeführt werden konnte. Aus diesem Grund spielte die Netzwerkleistung bei meinen Messungen *keine* Rolle.
  3. Ohne CPU- oder Netzwerkdrosselung begann ich, im Profiler aufzuzeichnen, und lud die Seite neu.
  4. Nach dem Seitenaufruf öffnete ich die mobile Navigation und schloss sie dann wieder.
  5. Ich stoppte den Profiler und zeichnete auf, wie viel CPU-Zeit für jedes der vier zuvor genannten Verhaltensweisen benötigt wurde.
  6. Ich löschte die Performance-Timeline. In Chrome klickte ich auch auf die Schaltfläche zur Speicherbereinigung, um jeden Arbeitsspeicher freizugeben, der durch den App-Code einer früheren Sitzung belegt gewesen sein könnte.

Ich wiederholte dieses Verfahren zehnmal für jedes Szenario auf jedem Gerät. Zehn Iterationen schienen gerade genug Daten zu liefern, um einige Ausreißer zu erkennen und ein einigermaßen genaues Bild zu erhalten, aber ich überlasse Ihnen die Entscheidung, während wir die Ergebnisse durchgehen. Wenn Sie kein Wortspiel mit meinen Erkenntnissen wünschen, können Sie die Ergebnisse in dieser Tabelle einsehen und Ihre eigenen Schlüsse ziehen, sowie den mobilen Navigationscode für jede Implementierung.

Die Ergebnisse

Ich wollte diese Informationen ursprünglich in einem Diagramm darstellen, aber aufgrund der Komplexität dessen, was ich gemessen habe, war ich mir nicht sicher, wie ich die Ergebnisse darstellen sollte, ohne die Visualisierung zu überladen. Daher präsentiere ich die minimalen, maximalen, medianen und durchschnittlichen CPU-Zeiten in einer Reihe von Tabellen, die alle effektiv die Bandbreite der Ergebnisse veranschaulichen, auf die ich bei jedem Test gestoßen bin.

Google Chrome auf Nokia 2

Das Nokia 2 ist ein kostengünstiges Android-Gerät mit einem ARM Cortex-A7 Prozessor. Es ist *kein* Kraftpaket, sondern ein billiges und leicht erhältliches Gerät. Die Android-Nutzung weltweit liegt derzeit bei etwa 40 %, und obwohl die Spezifikationen von Android-Geräten stark variieren, sind Low-End-Android-Geräte nicht selten. Dies ist ein Problem, das wir als eines sowohl von Reichtum *als auch* von der Nähe zu schneller Netzwerkinfrastruktur erkennen müssen.

Sehen wir uns die Zahlen für die Startkosten an.

Startzeit
React KomponentePreact KomponenteaddEventListener Code
Min137.2131.234.69
Median147.7642.065.99
Avg162.7343.166.81
Max280.8162.0312.06

Ich glaube, es sagt einiges aus, dass es im Durchschnitt über 160 ms dauert, um React zu parsen und zu kompilieren und *eine Komponente* zu hydrieren. Zur Erinnerung: Die *Startkosten* beinhalten in diesem Fall die Zeit, die der Browser benötigt, um die für die mobile Navigation benötigten Skripte auszuwerten. Für React und Preact beinhaltet dies auch die Hydrationszeit, die in beiden Fällen zu dem Uncanny-Valley-Effekt beitragen kann, den wir manchmal beim Start erleben.

Preact schneidet deutlich besser ab und benötigt etwa 73 % weniger Zeit als React, was angesichts der geringen Größe von Preact (10 KiB unkomprimiert) sinnvoll ist. Dennoch ist es wichtig zu beachten, dass das Frame-Budget in Chrome etwa 10 ms beträgt, um Ruckeln bei 60 fps zu vermeiden. Janky-Start ist genauso schlimm wie alles andere Janky und ist ein Faktor bei der Berechnung des First Input Delay. Alles in allem schneidet Preact jedoch relativ gut ab.

Was die addEventListener-Implementierung betrifft, so stellt sich heraus, dass die Parse- und Kompilierzeit für ein kleines Skript ohne Overhead erwartungsgemäß sehr gering ist. Selbst bei der maximalen Stichprobenzeit von 12 ms befinden Sie sich kaum im äußeren Ring des Ballungsraums Janksburg. Schauen wir uns nun die reine Hydrationskosten an.

Hydrationszeit
React KomponentePreact Komponente
Min67.0419.17
Median70.3326.91
Avg74.8726.77
Max117.8644.62

Für React liegt dies immer noch in der Nähe des Yikes Peak. Sicher, eine mittlere Hydrationszeit von 70 ms für *eine* Komponente ist kein großes Problem, aber denken Sie darüber nach, wie sich die Hydrationskosten skalieren, wenn Sie eine *Vielzahl* von Komponenten auf derselben Seite haben. Es ist keine Überraschung, dass sich die React-Websites, die ich auf diesem Gerät teste, eher wie Ausdauertests als wie Benutzererlebnisse anfühlen.

Die Hydrationszeiten von Preact sind deutlich kürzer, was sinnvoll ist, da die Dokumentation von Preact für seine hydrate-Methode besagt, dass sie „die meisten Vergleiche überspringt und dennoch Event-Listener anhängt und Ihren Komponentebaum einrichtet“. Die Hydrationszeit für das Szenario addEventListener wird nicht gemeldet, da Hydration bei VDOM-Frameworks keine Rolle spielt. Als Nächstes werfen wir einen Blick auf die Zeit, die zum Öffnen der mobilen Navigation benötigt wird.

Zeit zum Öffnen der mobilen Navigation
React KomponentePreact KomponenteaddEventListener Code
Min30.8911.943.94
Median43.6214.296.14
Avg43.1614.666.12
Max53.1920.468.60

Ich finde diese Zahlen *etwas* überraschend, da React fast siebenmal so viel CPU-Zeit benötigt, um einen Event-Listener-Callback auszuführen, als ein von Ihnen selbst registrierter Event-Listener. Das ist verständlich, da die Zustandsmanagementlogik von React notwendiger Overhead ist, aber man muss sich fragen, ob es sich für simple, lineare Interaktionen lohnt.

Auf der anderen Seite schafft es Preact, den Overhead bei Event-Listenern so weit zu begrenzen, dass die Ausführung eines Event-Listener-Callbacks nur doppelt so viel CPU-Zeit benötigt.

Die CPU-Zeit, die zum Schließen der mobilen Navigation benötigt wurde, war deutlich geringer, mit einer durchschnittlichen Zeit von etwa 16,5 ms für React, wobei Preact und reine Event-Listener etwa 11 ms bzw. 6 ms benötigten. Ich würde die vollständige Tabelle für die Messungen zum Schließen der mobilen Navigation posten, aber wir haben noch viel zu durchforsten. Außerdem können Sie sich diese Zahlen selbst in der bereits erwähnten Tabelle ansehen.

Ein kurzer Hinweis zu JavaScript-Samples

Bevor wir zu den iOS-Ergebnissen übergehen, möchte ich einen potenziellen Stolperstein ansprechen: die Auswirkungen des Deaktivierens von JavaScript-Samples in den Chrome DevTools beim Aufzeichnen von Sitzungen auf Remote-Geräten. Nach der Zusammenfassung meiner anfänglichen Ergebnisse fragte ich mich, ob der Overhead der Erfassung ganzer Aufrufstapel meine Ergebnisse verzerrte, also testete ich das React-Szenario erneut mit deaktivierten Samples. Wie sich herausstellte, hatte diese Einstellung keine signifikante Auswirkung auf die Ergebnisse.

Da die Aufrufstapel abgeschnitten waren, konnte ich die Komponenten-Hydrationszeit nicht messen. Die durchschnittlichen Startkosten mit deaktivierten Samples im Vergleich zu aktivierten Samples betrugen 160,74 ms und 162,73 ms. Die jeweiligen Medianwerte lagen bei 157,81 ms und 147,76 ms. Ich würde dies als eindeutig „im Rauschen“ bezeichnen.

Safari auf iPhone SE erster Generation

Das ursprüngliche iPhone SE ist ein großartiges Telefon. Trotz seines Alters erfreut es sich immer noch treuer Besitzer aufgrund seiner angenehmeren physischen Größe. Es wurde mit dem Apple A9 Prozessor ausgeliefert, der immer noch ein solider Konkurrent ist. Sehen wir uns an, wie es bei der Startzeit abgeschnitten hat.

Startzeit
React KomponentePreact KomponenteaddEventListener Code
Min32.067.630.81
Median35.609.421.02
Avg35.7610.151.07
Max39.1816.941.56

Dies ist eine deutliche Verbesserung gegenüber dem Nokia 2 und veranschaulicht die Kluft zwischen Low-End-Android-Geräten und älteren Apple-Geräten mit erheblicher Laufleistung.

Die Leistung von React ist immer noch nicht gut, aber Preact bringt uns in ein typisches Frame-Budget für Chrome. Event-Listener allein sind natürlich blitzschnell und lassen viel Raum im Frame-Budget für andere Aktivitäten.

Leider konnte ich die Hydrationszeiten auf dem iPhone nicht messen, da die Remote-Debugging-Sitzung jedes Mal abstürzte, wenn ich den Aufrufstapel in den DevTools von Safari durchlief. Da die Hydrationszeit eine Teilmenge der gesamten Startkosten war, können Sie davon ausgehen, dass sie mindestens die Hälfte der Startzeit ausmacht, wenn die Ergebnisse der Nokia 2-Tests ein Indikator sind.

Zeit zum Öffnen der mobilen Navigation
React KomponentePreact KomponenteaddEventListener Code
Min16.915.450.48
Median21.118.620.50
Avg21.0911.070.56
Max24.2019.791.00

React schneidet hier ganz gut ab, aber Preact scheint Event-Listener etwas effizienter zu handhaben. Reine Event-Listener sind blitzschnell, selbst auf diesem alten iPhone.

Safari auf iPhone SE zweiter Generation

Mitte 2020 kaufte ich das neue iPhone SE. Es hat die gleiche physische Größe wie ein iPhone 8 und ähnliche Telefone, aber der Prozessor ist derselbe Apple A13, der im iPhone 11 verwendet wird. Es ist *sehr* schnell für seinen relativ niedrigen Verkaufspreis von 400 US-Dollar. Angesichts eines solch leistungsstarken Prozessors, wie geht es damit um?

Startzeit
React KomponentePreact KomponenteaddEventListener Code
Min20.265.190.53
Median22.206.480.69
Avg22.026.360.68
Max23.677.180.88

Ich nehme an, irgendwann gibt es abnehmende Erträge, wenn es um die relativ geringe Arbeitslast des Ladens eines einzelnen Frameworks und des Hydrierens einer Komponente geht. Die Dinge sind auf einem iPhone SE der zweiten Generation in einigen Fällen etwas schneller als bei seinem Vorgängermodell, aber nicht dramatisch. Ich stelle mir vor, dass dieses Telefon größere und anhaltendere Workloads besser bewältigen würde als sein Vorgänger.

Zeit zum Öffnen der mobilen Navigation
React KomponentePreact KomponenteaddEventListener Code
Min13.1512.060.49
Median16.4112.570.53
Avg16.1112.630.56
Max17.5113.260.78

Leichte Verbesserung der React-Leistung hier, aber nicht viel mehr. Seltsamerweise scheint Preact auf diesem Gerät im Durchschnitt länger zu brauchen, um die mobile Navigation zu öffnen, als sein Vorgängermodell, aber das schreibe ich Ausreißern zu, die einen relativ kleinen Datensatz verzerren. Ich würde keinesfalls davon ausgehen, dass das iPhone SE der ersten Generation auf dieser Basis ein schnelleres Gerät ist.

Chrome auf einem veralteten Windows 10 Laptop

Zugegebenermaßen waren dies die Ergebnisse, auf die ich am meisten gespannt war: Wie bewältigt ein ASUS-Laptop von 2013 mit Windows 10 und einem damals aktuellen Ivy Bridge i5 diese Dinge?

Startzeit
React KomponentePreact KomponenteaddEventListener Code
Min43.1513.111.81
Median45.9514.542.03
Avg45.9214.472.39
Max48.9816.493.61

Die Zahlen sind nicht schlecht, wenn man bedenkt, dass das Gerät sieben Jahre alt ist. Der Ivy Bridge i5 war ein guter Prozessor für seine Zeit, und wenn man ihn mit der Tatsache kombiniert, dass er *aktiv gekühlt* wird (im Gegensatz zur *passiven Kühlung* von Prozessoren mobiler Geräte), stößt er wahrscheinlich nicht so oft auf Thermal-Throttling-Szenarien wie mobile Geräte.

Hydrationszeit
React KomponentePreact Komponente
Min17.757.64
Median23.558.73
Avg23.128.72
Max26.259.55

Preact schneidet hier gut ab, bleibt im Frame-Budget von Chrome und ist fast dreimal schneller als React. Die Situation könnte bei zehn Komponenten, die beim Start geladen werden, ganz anders aussehen, möglicherweise sogar bei Preact.

Zeit zum Öffnen der mobilen Navigation
Preact KomponenteaddEventListener Code
Min6.062.500.88
Median10.433.090.97
Avg11.243.211.02
Max14.444.341.49

Bei dieser isolierten Interaktion sehen wir eine Leistung, die mit High-End-Mobilgeräten vergleichbar ist. Es ist ermutigend zu sehen, wie ein so alter Laptop noch einigermaßen mithält. Allerdings springt der Lüfter dieses Laptops beim Surfen im Internet oft an, sodass die aktive Kühlung wahrscheinlich die Rettung dieses Geräts ist. Wenn der i5 dieses Geräts passiv gekühlt würde, vermute ich, dass seine Leistung sinken könnte.

Flache Aufrufstapel für den Sieg

Es ist kein Geheimnis, warum React und Preact länger zum Starten brauchen als eine Lösung, die Frameworks ganz vermeidet. Weniger Arbeit bedeutet weniger Verarbeitungszeit.

Obwohl ich die Startzeit für entscheidend halte, ist es wahrscheinlich unvermeidlich, dass man *einiges* an Geschwindigkeit für eine bessere Entwicklererfahrung eintauscht. Ich würde jedoch nachdrücklich argumentieren, dass wir viel zu oft zu viel in Richtung Entwicklererfahrung als Benutzererfahrung eintauschen.

Die Drachen liegen auch darin, was wir *nachdem* das Framework geladen ist, tun. Die Client-seitige Hydration ist etwas, das meiner Meinung nach viel zu oft missbraucht wird und manchmal völlig unnötig sein kann. Jedes Mal, wenn Sie eine Komponente in React hydrieren, werfen Sie dies auf den Haupt-Thread

A React stateful component hydration call stack captured in Chrome DevTools.

Erinnern Sie sich, dass auf dem Nokia 2 die *minimale* Zeit, die ich für die Hydration der mobilen Navigationskomponente gemessen habe, etwa 67 ms betrug. In Preact – für das Sie den Hydrations-Aufrufstapel unten sehen werden – dauert es etwa 20 ms.

A Preact stateful component hydration call stack captured in Chrome DevTools.

Diese beiden Aufrufstapel sind nicht auf derselben Skala, aber die Hydrationslogik von Preact ist vereinfacht, wahrscheinlich weil „die meisten Vergleiche übersprungen werden“, wie die Dokumentation von Preact besagt. Hier passiert deutlich weniger. Wenn Sie dem Metall näher kommen, indem Sie addEventListener anstelle eines Frameworks verwenden, können Sie noch schneller werden.

Ein Aufrufstapel von Event-Listenern, die an DOM-Elemente angehängt werden.

Nicht jede Situation erfordert diesen Ansatz, aber Sie werden überrascht sein, was Sie erreichen können, wenn Ihre Werkzeuge addEventListener, querySelector, classList, setAttribute/getAttribute und so weiter sind.

Diese Methoden – und viele weitere ähnliche – sind das, worauf Frameworks selbst angewiesen sind. Der Trick besteht darin, zu bewerten, welche Funktionalität Sie sicher außerhalb dessen liefern können, was das Framework bietet, und sich auf das Framework zu verlassen, wenn es sinnvoll ist.

Ein Aufrufstapel von React, der einen Klick-Handler auslöst, um eine mobile Navigation zu öffnen.

Wenn dies ein Aufrufstapel wäre, um beispielsweise Daten für eine API-Anfrage auf dem Client abzurufen und den komplexen Zustand der Benutzeroberfläche in dieser Situation zu verwalten, würde ich diese Kosten als akzeptabler empfinden. Aber das ist nicht der Fall. Wir lassen nur eine Navigation auf dem Bildschirm erscheinen, wenn der Benutzer auf eine Schaltfläche tippt. Es ist, als würde man einen Bulldozer verwenden, wenn eine Schaufel besser für den Job geeignet wäre.

Preact schlägt zumindest die goldene Mitte

Ein Aufrufstapel von Preact, der einen Klick-Event-Handler auslöst, um eine mobile Navigation zu öffnen.

Preact benötigt etwa ein Drittel der Zeit, um die gleiche Arbeit wie React zu erledigen. Aber auf diesem leistungsschwachen Gerät überschreitet es oft das Frame-Budget. Das bedeutet, dass das Öffnen dieser Navigation auf einigen Geräten träge animiert wird, da die Layout- und Zeichenarbeiten möglicherweise nicht genügend Zeit haben, um fertig zu werden, ohne in den Bereich der Long Tasks zu geraten.

Ein Aufrufstapel eines reinen Event-Listeners, der die mobile Navigation öffnet.

In diesem Fall war ein Event-Listener das, was ich brauchte. Er erledigt die Aufgabe auf dem leistungsschwachen Gerät siebenmal schneller als React.

Fazit

Dies ist kein Front gegen React, sondern eher ein Plädoyer für die Berücksichtigung unserer Arbeitsweise. Einige dieser Performance-Fallstricke können vermieden werden, wenn wir sorgfältig prüfen, welche Werkzeuge für die jeweilige Aufgabe sinnvoll sind, selbst für Anwendungen mit viel komplexer Interaktivität. Um fair zu React zu sein, existieren diese Fallstricke wahrscheinlich in vielen VDOM-Frameworks, da ihre Natur notwendige Overhead-Kosten mit sich bringt, um all diese Dinge für uns zu verwalten.

Selbst wenn Sie an etwas arbeiten, das kein React oder Preact erfordert, aber Sie die Vorteile der Komponentisierung nutzen möchten, sollten Sie in Erwägung ziehen, alles zunächst auf dem Server zu belassen. Dieser Ansatz bedeutet, dass Sie entscheiden können, ob und wann es angebracht ist, Funktionalität auf den Client auszulagern – und *wie* Sie dies tun werden.

Im Falle meiner RSS-Feed-App kann ich dies bewerkstelligen, indem ich leichtgewichtigen Event-Listener-Code in den Entry Point für diese Seite der App lege und ein Asset-Manifest verwende, um die minimale Menge an Skript einzubinden, die für die Funktionalität jeder Seite erforderlich ist.

Nehmen wir nun an, Sie haben eine Anwendung, die *wirklich* das benötigt, was React bietet. Sie haben komplexe Interaktivität mit viel Zustand. Hier sind einige Dinge, die Sie tun können, um zu versuchen, Dinge etwas schneller zum Laufen zu bringen.

  1. Überprüfen Sie alle Ihre zustandsbehafteten Komponenten – also jede Komponente, die von React.Component erbt – und sehen Sie, ob sie als zustandslose Komponenten refaktorisiert werden können. Wenn eine Komponente keine Lifecycle-Methoden oder Zustand verwendet, können Sie sie als zustandlos refaktorieren.
  2. Vermeiden Sie dann, wenn möglich, das Senden von JavaScript an den Client für diese zustandlosen Komponenten sowie deren Hydration. Wenn eine Komponente zustandlos ist, rendern Sie sie nur auf dem Server. Rendern Sie Komponenten vorab, wann immer möglich, um die Serverantwortzeit zu minimieren, da Server-Rendering eigene Performance-Fallstricke birgt.
  3. Wenn Sie eine zustandsbehaftete Komponente mit einfacher Interaktivität haben, erwägen Sie, diese Komponente vorab zu rendern/serverseitig zu rendern und ihre Interaktivität durch Framework-unabhängige Event-Listener zu ersetzen. Dies vermeidet die Hydration vollständig, und Benutzerinteraktionen müssen nicht durch die Zustandsverwaltungslogik des Frameworks gefiltert werden.
  4. Wenn Sie zustandsbehaftete Komponenten auf dem Client hydrieren müssen, erwägen Sie, Komponenten, die sich nicht am oberen Rand der Seite befinden, verzögert zu hydrieren. Ein Intersection Observer, der einen Callback auslöst, funktioniert hierfür sehr gut und gibt dem Hauptthread mehr Zeit für kritische Komponenten auf der Seite.
  5. Für verzögert zu hydrierende Komponenten prüfen Sie, ob Sie deren Hydration während der Hauptthread-Leerlaufzeit mit requestIdleCallback planen können.
  6. Wenn möglich, erwägen Sie, von React zu Preact zu wechseln. Angesichts der Tatsache, wie viel schneller es auf dem Client läuft als React, lohnt es sich, die Diskussion mit Ihrem Team zu führen, ob dies möglich ist. Die neueste Version von Preact ist für die meisten Dinge fast 1:1 mit React kompatibel, und preact/compat erleichtert diesen Übergang hervorragend. Ich glaube nicht, dass Preact ein Allheilmittel für Performance ist, aber es bringt Sie näher dorthin, wo Sie hin müssen.
  7. Berücksichtigen Sie die Anpassung Ihrer Benutzererfahrung an Benutzer mit wenig Gerätespeicher. navigator.deviceMemory (verfügbar in Chrome und abgeleiteten Browsern) ermöglicht es Ihnen, die Benutzererfahrung für Benutzer auf Geräten mit wenig Speicher zu ändern. Wenn jemand ein solches Gerät hat, ist es wahrscheinlich, dass auch sein Prozessor nicht sehr schnell ist.

Was auch immer Sie mit diesen Informationen entscheiden, der Kern meines Arguments ist: Wenn Sie React oder eine VDOM-Bibliothek verwenden, sollten Sie einige Zeit damit verbringen, deren Auswirkungen auf verschiedene Geräte zu untersuchen. Besorgen Sie sich ein günstiges Android-Gerät und sehen Sie, wie sich Ihre App anfühlt. Vergleichen Sie diese Erfahrung mit Ihren High-End-Geräten.

Vor allem folgen Sie nicht den "Best Practices", wenn das Ergebnis ist, dass Ihre App einen Teil Ihres Publikums, das sich keine High-End-Geräte leisten kann, effektiv ausschließt. Drängen Sie weiterhin darauf, dass alles schneller wird. Wenn unsere tägliche Arbeit irgendeinen Hinweis gibt, ist dies ein Unterfangen, das Sie eine Weile beschäftigen wird, aber das ist in Ordnung. Das Web schneller zu machen, macht das Web an mehr Orten zugänglicher. Das Web zugänglicher zu machen, macht das Web *inklusiver*. Das ist die wirklich gute Arbeit, die wir alle mit aller Kraft tun sollten.


Ich möchte Eric Bailey für sein redaktionelles Feedback zu diesem Beitrag sowie dem CSS-Tricks-Team für seine Bereitschaft, ihn zu veröffentlichen, meinen Dank aussprechen.