Startseite / Artikel

Wenn die React-Memoisierung ins Gegenteil umschlägt: Warum weniger Renderungen die App langsamer machten.

Wenn die React-Memoisierung ins Gegenteil umschlägt: memo, useMemo und useCallback führten zu zusätzlichen Kosten sowie veralteten Schließfunktionen auf einer großen Seite. Durch das Verschieben des Zustands und das Profilieren des Ablaufs wurde das Problem gelöst.

Verlag: REACTAPP.TOP13 Min. Lesezeit2888 Wörter

Quellen: The React Optimization That Made Our App Slower

Ein Fall des Behebens des Falschen

Stellen Sie sich eine große Seite vor, die jedes Mal neu gerendert wird, wenn jemand in ein Filterfeld eintippt. Sie enthält Tabellen, Diagramme, Zusammenfassungen sowie mehrere interaktive Panels, und der React Profiler zeigt eine lange Liste von Komponenten an. Das sieht aus wie der Beweis dafür, dass React viel zu viel Arbeit leistet.

Die übliche Lösung wird angewandt: Komponenten werden mit memo umhüllt, Handler mit useCallback, Berechnungen mit useMemo, und Selektoren erhalten benutzerdefinierte Gleichheitsfunktionen. Eine Komponente, die weiterhin gerendert wird, erhält weitere Stabilisierungen ihrer Props. Ein Objekt, dessen Referenz sich ändert, wird gememoisiert. Ein fehlgeschlagener Vergleich wird durch einen detaillierteren ersetzt.

Letztendlich zeigt der Profiler tatsächlich weniger Komponentenaufrufe an. Die App fühlt sich jedoch nicht schneller an. Einige Interaktionen reagieren langsamer, einige Bildschirme verbrauchen mehr Speicher, und einige Komponenten zeigen veraltete Werte, weil eine Abhängigkeit weggelassen wurde, um eine Referenz stabil zu halten. Auch das Debuggen wird schwieriger, da die Leistung nun von Caches und Vergleichsregeln abhängt, die im gesamten System verteilt sind. Das Team hatte die leicht erkennbare Zahl angepasst und nicht das, worauf die Nutzer tatsächlich warteten.

Die Verwendung von Memoisierung war nicht der Fehler, da React solche Werkzeuge bietet, um echte Probleme zu lösen. Der Fehler lag in der Annahme, dass das Stoppen einer Renderung immer besser sei als ihre Zulassung. Das Rendern erfordert Aufwand, genauso wie die Entscheidung, ob man darauf verzichten soll. Wenn diese Entscheidung dazu führt, Abhängigkeitsarrays neu aufzubauen, große Props zu vergleichen, alte Werte beizubehalten und spezielle Cache-Logik auszuführen, kann die Optimierung am Ende teurer sein als die Arbeit, die sie eigentlich ersetzen sollte.

Was „unnötig“ wirklich bedeutet

Der erste Fehler lag in der Definition einer unnötigen Renderung. Ein Komponente erschien im Profiler, obwohl ihre endgültige DOM-Ausgabe sichtbar nicht geändert worden war, weshalb die Renderung als Verschwendung eingestuft wurde. Das ignoriert, wie günstig viele Komponenten tatsächlich sind.

Nehmen wir ein Funktionskomponente, die einige primitive Props liest und einen einfachen Elementbaum zurückgibt. Sie wird in kürzester Zeit ausgeführt, React vergleicht die Ausgabe miteinander und ändert im DOM fast nichts – daher bringt das Blockieren dieser Aufrufe nahezu keinen Vorteil.

Die gememorierte Version hat ihre eigene Aufgabe. React muss die vorherigen Props speichern, die neuen erhalten und sie miteinander vergleichen. Bei mehreren Props, wobei einige Objekte, Arrays oder Funktionen sind, kann der Vergleich oft fehlschlagen oder so aufwendig werden, dass alle Vorteile zunichte gemacht werden. Die Anzahl der Renderungen wurde faktisch zu einem Leistungsindikator – je niedriger sie war, desto besser, selbst wenn die ausgelassenen Renderungen nicht die Ursache für die Verzögerung waren.

Es handelt sich um eine einfache Falle, denn der Profiler macht die Renderung sichtbar, während andere Kosten verborgen bleiben. Der Vergleich von Props, die Zuweisung von JavaScript, die Beibehaltung im Cache, Store-Selektoren, die Layout-Generierung, das Zeichnen sowie Drittanbieter-Bibliotheken können eine Interaktion dominieren, während die Anzahl der Renderungen die gesamte Aufmerksamkeit auf sich zieht. Ein erneutes Rendern sollte vermieden werden, wenn das Komponenten eine sinnvolle Arbeit ausführt und seine Eingaben meist unverändert bleiben. Es lohnt sich jedoch nicht, es nur deshalb zu vermeiden, weil es bereits aufgetreten ist.

Was memo tatsächlich kostet

Menschen beschreiben memo oft als Schutzmechanismus, der verhindert, dass ein Kind-Komponenten erneut gerendert wird, wenn sich die Props nicht geändert haben. Diese Beschreibung lässt einen Schritt aus: React muss zunächst die alten Props mit den neuen vergleichen, bevor es etwas überspringen kann.

Mit primitiven Eigenschaften ist das Vergleichen einfach. Bei umfangreichen Komponentenverträgen ist die Kostenabschätzung schwieriger. Zeilenobjekte, Konfigurationsarrays, Berechtigungsmodelle, Callback-Listen sowie verschachtelte Optionen wurden alle übergeben. Das Hüllen des Empfängers in memo stabilisierte keine dieser Werte – es fügte lediglich eine weitere Stufe hinzu, in der React überprüfte, ob sie stabil waren.

Die meisten dieser Überprüfungen scheiterten, weil der Elternteil während der Renderung mindestens eine Referenz neu erstellte. React verglich zwar, rendernte aber dennoch weiter – dadurch entstand zusätzliche Arbeit, ohne dass etwas entfernt wurde.

Es kamen benutzerdefinierte Vergleichsfunktionen zum Einsatz, die dazu dienten, die Werte genauer zu überprüfen. Ein Komponenten-Element der Datenmatrix prüfte seine Spalten, welche Zeilen ausgewählt waren, die Details zur Seitenleiste, ob gerade geladen wurde, sowie die Zeilen selbst. Dieser Vergleichsmechanismus wurde zu einer zweiten Kopie des Abhängigkeitsmodells der Komponente und konnte auf beide Arten versagen: Ein gründlicher Vergleich kostete Rechenzeit, während ein ungenauer Vergleich eine notwendige Darstellung überspringen könnte.

Nun existierte eine neue Grenze für die Korrektheit. Alles, was die Darstellung beeinflusste, musste im Vergleichsmechanismus widergespiegelt werden, wodurch das Hinzufügen einer Eigenschaft bedeutete, die Komponente samt ihrer Memoisierungslogik gemeinsam zu ändern. Komponenten, die ursprünglich standardmäßig korrekt dargestellt wurden, blieben nur dann in Ordnung, solange eine manuell gepflegte Gleichheitsfunktion sie fehlerfrei beschrieb.

Tiefer Vergleich

Wenn oberflächliche Vergleiche zu oft fehlschlagen, scheint ein tiefer Vergleich der offensichtliche nächste Schritt zu sein. Wenn zwei Konfigurationsobjekte dieselben Werte enthalten, warum sollten unterschiedliche Referenzen dann zu einer Neuzeichnung zwingen? Vergleichen Sie den Inhalt und überspringen Sie die Überprüfung, wenn die Struktur identisch ist.

Das funktioniert in Demos mit kleinen Objekten. In einer echten Anwendung enthalten einige Eigenschaften Hunderte oder Tausende von Werten, und ein tiefer Vergleich durchläuft Arrays, prüft verschachtelte Objekte und wiederholt dies bei jeder Aktualisierung des übergeordneten Elements. Das geschützte Komponente kann nur einige wenige Zeilen ausgeben oder eine Komponente von Drittanbietern zurückgeben, die ihre eigene Memoisierung für stabile Unterstrukturen verwendet. Die Gleichheitsprüfung kann teurer sein als der Aufruf der Komponente selbst.

Ein tiefer Vergleich skaliert außerdem mit der Größe der Eingabe und nicht mit der Größe der Änderung. Eine einzige kleine Aktualisierung des Zustands kann den Vergleicher dazu bringen, eine ganze Sammlung durchzugehen, um nachzuweisen, dass der größte Teil davon sich nicht geändert hat.

Die Struktur ist auch nicht immer eine zuverlässige Grundlage für Vergleiche. Funktionen, Datumsangaben, Karten, Klasseninstanzen und Bibliothekselemente können Verhaltensweisen oder Identitäten aufweisen, die eine allgemeine Funktion zur Prüfung der tiefen Gleichheit nicht interpretieren kann. Daher kann selbst ein scheinbar umfassender Vergleich veraltetes Verhalten beibehalten. Das Team hatte Reacts einfaches Referenzmodell zugunsten eines benutzerdefinierten Systems zur semantischen Gleichheit aufgegeben, ohne jemals definiert zu haben, was Gleichheit für jeden Domänenwert bedeutet.

Die bessere Lösung bestand selten darin, instabile Objekte intensiver miteinander zu vergleichen. Vielmehr ging es darum, ihre Identität bei unveränderten zugrundeliegenden Daten natürlich beizubehalten, den Vertragsrahmen der Eigenschaften einzugrenzen oder die Aktualisierung näher an das Komponente zu verlegen, das sie verwaltet.

Werte speichern

Dann kamen die Werte. Gefilterte Arrays, Konfigurationsobjekte, formatierte Beschriftungen, Abfragemaps sowie einfache Berechnungen erhielten alle useMemo, sobald die umgebenden Komponenten bereits gememoisiert worden waren.

Einige davon verdienten das tatsächlich. Das Sortieren einer großen Sammlung oder das Erstellen eines aufwändigen Modells lohnt sich durch Caching, wenn sich die Eingaben seltener ändern als die Elternkomponente neu gerendert wird. Viele andere hingegen nicht. Das Zusammenfügen zweier Zeichenketten oder das Erstellen eines kleinen Objekts ist kostengünstiger als die dazugehörige Infrastruktur: Die Komponente erstellt weiterhin ein Abhängigkeitsarray, React vergleicht weiterhin jeden Eintrag mit dem vorherigen, das vorherige Ergebnis belegt weiterhin Speicher und Entwickler müssen weiterhin prüfen, ob die Liste vollständig ist.

Auch der Code wurde visuell aufwendiger. Ein Wert, der früher bereits in einer einzigen Ausdrucksform seine Bedeutung klar machte, musste nun in einem Hook sowie einem Abhängigkeitsarray untergebracht werden.

const label = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);

Während der Ausführung fügte der Cache fast nichts hinzu, und das deutete darauf hin, dass die Berechnung wichtig genug war, um optimiert zu werden. Noch schlimmer war, dass die Verwendung von useMemo überall den Datenfluss trübte. Entwickler hielten Objektreferenzen stabil – nicht weil es kostspielig wäre, die Objekte zu erstellen, sondern weil ein anderer, gememorisierter Komponente auf ihre Identität angewiesen war. Jede Optimierung erforderte weitere, wodurch ein Cache-Tree entstand, der nur dann funktionierte, solange alle Verbindungen bestanden.

Memoisierung lohnt sich am meisten, wenn sie aufwändige Arbeiten bewahrt. Hier ging es jedoch darum, die Referenzen zu bewahren, die wiederum eine andere Memoisierung erforderten – was ein völlig anderes Ziel ist.

Callbacks und veraltete Schließungen

Callbacks, die während der Darstellung erstellt werden, erhalten jedes Mal eine neue Identität. Wenn sie an gememoriserte Kinderkomponenten weitergegeben werden, führen diese neuen Referenzen dazu, dass der Vergleich der Kinderkomponenten fehlschlägt. Die Lösung bestand darin, fast jeden Handler in useCallback zu packen.

Dadurch verringerte sich zwar etwas die Anzahl der Referenzänderungen, doch es entstanden längere Abhängigkeitslisten. Ein Speichervorgang hing von dem Entwurf, dem ausgewählten Datensatz, dem aktuellen Benutzer, Berechtigungen, einem Benachrichtigungsdienst sowie der Mutationsfunktion ab, und jede Änderung an einem dieser Elemente führte ohnehin zu einem neuen Callback. Der Callback blieb selten lange genug stabil, um wiederverwendet zu werden. Das Kind wurde dennoch dargestellt, und der Elternteil trug einen weiteren Hook sowie einen weiteren Abhängigkeitsvertrag.

Entwickler entfernen manchmal Abhängigkeiten, um Callbacks stabil zu halten, was zu veralteten Schließungen führt. Ein Callback liest weiterhin einen alten Entwurf, einen veralteten Berechtigungswert oder ein zuvor ausgewähltes Element, weil die Beibehaltung der Identität wichtiger geworden ist als die Beibehaltung des aktuellen Verhaltens. Solcher Code kann die Tests entlang des ursprünglichen Interaktionspfads bestehen und erst nach einem Zustandswechsel ohne Neuerstellung des Callbacks fehlschlagen. Solche Fehler sind schwierig zu beheben, da die Funktion weiterhin existiert und ohne Fehler ausgeführt wird – sie handelt lediglich auf der Grundlage der Historie. Die Identität des Callbacks wurde auf Kosten der Aktualität der Daten optimiert.

Eine bessere Frage ist, ob das Kind überhaupt eine stabile Callback-Funktion benötigt. Bei einem günstigen, nicht gememorierten Child bringt useCallback kaum Vorteile. Wenn der Callback eine Abonnementfunktion oder eine teure, gememorierte Funktion versorgt, ist Stabilität wichtig, und seine Abhängigkeiten können oft durch Umschichtung der Verantwortlichkeiten, Übermittlung einer eingeschränkteren Operation oder Platzierung des Verhaltens näher am Zustand, den es liest, reduziert werden. Eine Funktion mit fester Identität ist von Natur aus nicht schneller – sie hilft nur, wenn etwas Sinnvolles sie wiederverwendet.

Caches verbrauchen Speicher

Durch Caching werden wiederholte Berechnungen vermieden, indem frühere Ergebnisse gespeichert werden, und diese Speicherung kostet etwas. Ein kleiner, gememorierter Wert ist selten allein schon ein Problem hinsichtlich des Speicherverbrauchs. In einer großen Anwendung können jedoch viele komponenten Arrays, Maps, transformierte Datensätze, formatierte Modelle sowie Schließfunktionen enthalten, die weitere Daten erfassen.

Dies ist ein Grund dafür, warum eine einzige Profilansicht die Leistung nicht beschreiben kann. Eine im Cache gespeicherte Berechnung kann aus der Darstellungszeitlinie verschwinden, während sie später zu einem Anstieg des Speicherverbrauchs und längeren Pausen führt. Ob sich das lohnt, hängt davon ab, wie groß das Ergebnis ist, welche Kosten für ein Neubauen anfallen, wie häufig es wiederverwendet wird und wie lange das Komponente geladen bleibt. Ein großes abgeleitetes Modell, das für ein selten geöffnetes Panel im Cache gespeichert wird, kann teurer sein als sein Neubau, wenn er benötigt wird. Caches sind keine kostenlosen Speicherplätze für alte Ergebnisse. Sie ersetzen Berechnungen durch beibehaltene Zustände sowie zusätzliche Logik zur Invaliderung.

Das eigentliche Problem lag darin, wo der Zustand gespeichert war

Die wichtigste Erkenntnis war, dass die erneuten Darstellungen davon abhingen, wo der Zustand platziert war. Ein ständig wechselnder Filter befand sich in der Seitenkomponente, die außerdem die Diagramme, Zusammenfassungen, eine große Tabelle sowie mehrere Panels darstellte. Jeder Tastendruck führte zu einer erneuten Darstellung der Seite und zum Neubau der Eigenschaften für jedes Kindkomponente.

Durch die Nutzung der Memoisierung zur Schutz der Kinder bedeutete dies die Stabilisierung ihrer Arrays, Objekte und Callbacks sowie benutzerdefinierter Selektoren und Vergleiche, während die Architektur weiterhin um eine einzige große Aktualisierungsgrenze kreiste. Durch das Verschieben des Filterzustands direkt neben die Tabelle verschwand das größte Problem, und der Rest der Seite musste sich nicht mehr selbst schützen.

Auch an anderen Stellen geschah dasselbe: Der Zustand von Dialogformularen befand sich im Screen-Component, der Hover-Zustand galt einer ganzen Sammlung von Karten, ein Kontext enthielt Werte mit sehr unterschiedlichen Aktualisierungsraten, und ein globaler Store-Selektor lieferte mehr Informationen, als sein Verbraucher benötigte. Die Memoisierung überdeckte lediglich die Entscheidungen bezüglich der Zustandsverwaltung, die dazu führten, dass kleine Änderungen zu weit verbreitet wurden.

Nachdem diese Grenzen verschärft wurden, wurden viele Memoisierungsschichten überflüssig. Komponenten, die nicht auf dem Aktualisierungsweg lagen, hörten auf, dargestellt zu werden, da der Zustand nicht mehr über ihnen lag, und Kinderkomponenten erhielten einfachere Kontrakte, weil die Elternkomponenten sie nicht mehr mikroverwalteten.

Weniger Darstellungen, gleiche Browserlast

Auch dort, wo die Memoisierung tatsächlich React-Darstellungen übersprang, blieben Interaktionen manchmal langsam, weil React nicht die Hauptkostenquelle war.

Eine Diagrammbibliothek führte weiterhin kostspielige imperative Aktualisierungen durch. Ein großer DOM-Baum benötigte weiterhin eine Neuanordnung, sobald sich die Abmessungen änderten. Ein CSS-Effekt erzwang umfangreiche Neudarstellungen, ein synchroner Speicherzugriff blockierte den Hauptthread, und ein Eingabehandler parsete ein großes Dokument, bevor der nächste Zeichen erscheinen konnte. Die Komponenten hatten nur Aufmerksamkeit erregt, weil sie von dem Profiler gemessen wurden, und da die Darstellung günstiger wurde, wurden die dahinterliegenden Browseraufgaben sichtbar.

Die Leistung von React ist nur ein Aspekt der Frontend-Leistung und ersetzt diese nicht. Selbst bei Verwendung der Memoisierung kann eine geänderte Abhängigkeit eines Effects dazu führen, dass ein Widget von Drittanbietern neu generiert wird. Das Überspringen der Renderung eines Elternelements verkürzt die Dekodierzeit eines zu großen Bildes nicht. Eine stabile Callback-Funktion behebt nicht das Problem des Layout-Instabilitäts durch das Lesen und Schreiben von Elementmaßen in der falschen Reihenfolge. Die Abläufe müssen im gesamten JavaScript-Stack verfolgt werden – von der React-Renderung über DOM-Änderungen, Stilberechnungen, Layout-Optimierungen, das Zeichnen bis hin zum Netzwerk – denn eine Beschleunigung auf einer Ebene kann den gesamten Prozess unverändert lassen. Während der Browser weiterhin beschäftigt war, blieb React still.

Schwerer zu ändernder Code

Der Leistungsverlust war nur die halbe Katastrophe – die Anwendung wurde zudem schwieriger in der Wartung.

Jeder Grenzbereich einer Komponente stellte neue Fragen. Wird die Komponente gememorisert? Welche Props benötigen stabile Referenzen? Deckt ein benutzerdefinierter Vergleicher den neuen Prop ab? Wurde ein Wert absichtlich aus der Abhängigkeitsliste weggelassen? Wird das Ändern der Struktur eines Objekts mehrere Caches ungültig machen? Für routinemäßige Entwicklungsarbeiten war ein Verständnis von Optimierungsregeln erforderlich, die nichts mit dem Verhalten des Produkts zu tun hatten.

Stellen Sie sich vor, ein Entwickler fügt einem Konfigurationsobjekt ein Feld hinzu und löst versehentlich umfassende Neuladungen aus. Ein Kollege „behebt“ das Problem mit einem detaillierteren Vergleich. Später fügt jemand eine Funktion zum Objekt hinzu und stellt fest, dass der Vergleicher zwei eindeutig unterschiedliche Konfigurationen als gleich behandelt. Der Code war an seine aktuelle Struktur angepasst, und jede Änderung störte ein fein abgestimmtes Netzwerk aus Referenzen und Abhängigkeitsarrays.

Eine gute Leistungsarchitektur macht häufige Änderungen günstiger und zwingt nicht dazu, dass jede Änderung versteckte Cache-Voraussetzungen berücksichtigt. Das bedeutet jedoch nicht, dass optimierter Code einfach bleiben muss. Einige kostspielige Interaktionen rechtfertigen tatsächlich spezialisierte Memoisierung sowie sorgfältige Handhabung von Referenzen. Die Komplexität sollte proportional zum erzielten Nutzen sein und sich auf bekannte Hotspots konzentrieren, während sie hier aufgrund der Annahme, dass jede Darstellung eines Tages zu einem Hotspot werden könnte, überall verteilt war.

Die Messung der Interaktionen änderte die Entscheidung

Der Wendepunkt war die Profilierung vollständiger Benutzerinteraktionen anstelle der Zählung der Darstellungen.

Die von den Nutzern als langsam bezeichneten Abläufe wurden aufgezeichnet und untersucht, um herauszufinden, wohin die Zeit ging. Mehrere Komponenten wurden häufig gerendert, nutzten aber fast keine Rechenleistung. Einige Berechnungen sowie Widgets von Drittanbietern dominierten, und benutzerdefinierte Vergleiche wurden als echte JavaScript-Arbeit erfasst. An einigen Stellen führte das Entfernen der Memoisierung zu etwas mehr Renderungen, doch zu weniger Gesamtzeit.

Zu Beginn schien das falsch zu sein, denn das gängige mentale Modell betrachtete weniger Neurenderungen als Fortschritt. Dem Browser ist es egal, wie oft die Funktionen der Komponenten aufgerufen werden – wichtig ist vielmehr, wie viel Arbeit vor dem nächsten responsiven Frame erledigt wird.

Von da an wurden Änderungen anhand der Gesamtkosten der Interaktion bewertet. Reagierte die Eingabe schneller? Wurde der Hauptthread frei? Bleibte der Speicher über lange Sitzungen hinweg konstant? Besserten sich die Produktionsversionen – und nicht nur die Entwicklerversionen? Auch die Fehlerbehebungen wurden lokalisiert: Eine kostspielige Transformation, ein großer Unterbaum oder eine instabile Bibliothekskonfiguration konnten direkt behoben werden, während der Rest der Anwendung auf ihre defensive Memoisierung verzichtete. Profiling führte nicht zu einem Neudesign – es verhinderte lediglich, dass das Team einem sichtbaren Wert nachjagte, der nicht mit der Benutzererfahrung zusammenhing.

Was blieb

Nicht jedes memo, useMemo und useCallback wurde entfernt, da dies den Fehler nur umgekehrt wiederholen würde.

Die Memoisierung wurde weiterhin auf teure Komponenten angewandt, deren Props meist unverändert blieben. Transformationen, die sich als kostspielig erwiesen und bei häufigen Renderungen der Elternkomponenten wiederverwendet wurden, blieben im Cache, und Callbacks blieben stabil, wo ihre Identität für Abonnements, Integrationen mit Drittanbietern oder sinnvolle Memoisierungsgrenzen wichtig war.

Was verschwand: Caches für triviale Berechnungen, benutzerdefinierte Vergleichsfunktionen, die umfassende strukturelle Überprüfungen durchführten, sowie die Zurückhaltung, billige Komponenten rendern zu lassen. Häufig sich ändernder Zustand wurde näher an die Schnittstellen gebracht, die ihn verwendeten, und Kontextwerte mit wenig Gemeinsamem wurden getrennt. Das Ergebnis war nicht die möglichst niedrige Anzahl an Renderungen. Es handelte sich um ein Update-Modell, das leichter verständlich war, wobei Optimierungen dort angewandt wurden, wo dies durch Belege gerechtfertigt war. Dadurch wurde die App sowohl schneller als auch einfacher zu ändern.

Rendering ist nicht der Feind

Diese Geschichte begann mit einem durchaus vernünftigen Instinkt: zu beobachten, wie Arbeit erledigt wird, und versuchen, sie zu stoppen. Der Fehler bestand darin, jede Ausführung als Verschwendung und jede übersprungene Ausführung als Erfolg zu betrachten.

Die Memoisierung vergleicht Eingaben, speichert Werte, verfolgt Abhängigkeiten und muss bei Codeänderungen weiterhin korrekt funktionieren. Eine benutzerdefinierte Gleichheitsprüfung kann teurer sein als eine Ausführung. Stabile Callbacks können veralteten Zustand einfrieren. Gecachte Ergebnisse beanspruchen Speicher. Und ein Wirrwarr aus Optimierungen kann es schwierig machen, einen Komponentenbaum zu ändern, ohne dass die Nutzer einen Vorteil bemerken.

Das Rendering in React ist oft kostengünstig. Schwache Zustandsverwaltung, wiederholte teure Berechnungen, umfangreiche Abonnements, Updates von Drittanbietern sowie die Browser-Layout-Logik spielen häufig eine weitaus größere Rolle. Der effektivste Ansatz bestand darin, billige Aufgaben weiterhin billig zu halten: Man sollte aufhören, gewöhnliche Komponenten in Caches zu verpacken, und sich stattdessen auf die wenigen wirklich teuren Operationen konzentrieren. Optimierungen sollten die Gesamtkosten einer Interaktion senken – nicht nur die Anzahl der Male, zu denen React eine Funktion aufruft. Die Geschwindigkeit entstand erst, als sich die Frage von der Art und Weise, Reacts Rendering zu stoppen, auf die Ursache dafür verlagerte, warum jede Aktualisierung überhaupt teuer wurde.

Verwandte Artikel

  • Warum das Frontend nach React schwierig bleibt: State, Effects, Grenzen und KI-Tools — React hat die Erstellung von Benutzeroberflächen erleichtert, nicht deren Gestaltung. Wo der State liegt, sind verwickelte Effects, Systemgrenzen sowie von KI erzeugter Code die eigentlichen Herausforderungen im Frontend-Entwicklung.
  • Hinzufügen eines Splash-Screens zu einer React Native-App für iOS und Android — Schritt für Schritt Anleitung zur Einrichtung eines Splash-Screens für React Native auf iOS und Android: Xcode-Startbildschirm, Android-Themen und -Layouts sowie react-native-splash-screen, um das weiße Flackern zu vermeiden.
  • 30 React- und Redux-Gewohnheiten für skalierbare, wartbare Frontends — Dreißig kurze Tipps zu React und Redux zu Themen wie State, Serverdaten, Hooks, Selectoren, Ordnerstruktur, Testing und Refactoring, geeignet für Anwendungen mit mehr als tausend Komponenten.