Neun häufige Muster, die unnötige React-Neuberechnungen auslösen.
Erklärt neun alltägliche React-State- und Effect-Muster, die heimlich den Umfang der Neuberechnung erweitern, sowie wie Komponenten umstrukturiert werden können, um Updates lokal zu halten.
Quellen: 9 React Habits That Quietly Cause Unnecessary Re-Renders
React läuft in der Regel schnell von selbst. Probleme entstehen, wenn eine kleine Aktualisierung dazu führt, dass ein weitaus größerer Teil des Komponentenbaums neu bewertet werden muss, als es die tatsächliche Änderung erfordert.
Lange Zeit schien es sinnvoll, übermäßiges Neuzeichnen als etwas zu betrachten, das React selbst beheben musste. Wenn eine Komponente häufiger aktualisiert wurde, als nötig, war die erste Reaktion, sie in memo zu packen oder useMemo und useCallback zu verwenden. Falls die Leistung immer noch langsam blieb, wurden auch die darüber liegenden Komponentenschichten auf dieselbe Weise behandelt, in der Hoffnung, dass React schließlich zur Ruhe kommen würde.
Diese Art der Patches ließ die Profilgrafiken manchmal besser aussehen, machte aber selten die zugrunde liegende Komponentenstruktur klarer. Die State-Ebene befand sich weiterhin höher im Baum, als nötig, Kontextwerte wurden weiterhin zusammengefasst, abgeleitete Daten dupliziert, Referenzen blieben instabil und Effects lösten weiterhin sofortige Aktualisierungen aus, sobald sie ausgeführt wurden. Die Memoisierung diente im Grunde dazu, die Benutzeroberfläche vor einem Datenfluss zu schützen, der breiter war, als nötig.
Eine erneute Darstellung an sich ist kein Fehler. React ist so konzipiert, dass er Ihre Komponentenfunktionen immer dann erneut aufruft, wenn sich ihre Eingaben geändert haben könnten, und viele Komponenten sind so kostengünstig, dass das Blockieren jeder harmlosen Darstellung mehr Komplexität verursacht, als es einspart.
Die eigentliche Frage ist nicht, ob eine Komponente erneut dargestellt wurde. Es geht darum, warum diese Darstellung ausgelöst wurde und welche Aufgaben ausgeführt wurden, nachdem React sie aufgerufen hatte.
Die meisten vermeidbaren erneuten Renderungen gehen auf Gewohnheiten zurück, die bei kleinen Komponenten harmlos erscheinen. Ihre Kosten zeigen sich erst später, wenn die Komponente wächst, ihre Kinder schwerer werden oder ein schnell ändernder Wert einen großen Teil der Seite ungültig macht.
1. Häufig ändernde Zustände zu hoch im Baum platzieren
Den Zustand auf einen gemeinsamen Vorfahren zu heben, ist eines der ersten Muster, die React-Entwickler erlernen. Wenn zwei Komponenten auf denselben Wert angewiesen sind, sorgt die Zuweisung an ihren nächsten gemeinsamen Elternteil dafür, dass sie synchron bleiben und doppelte Quellen der Wahrheit vermieden werden.
Das wird zum Problem, wenn jeder kleine Teil des lokalen Interaktionszustands einfach auf die Komponente auf Ebene der Seite hochgepusht wird, nur weil diese zufällig Zugriff auf alles hat.
Ein Suchfeld behält seinen Textwert auf dem Dashboard bei. Das Öffnungs-/Schließungs-Symbol eines Dropdown-Menüs befindet sich neben den Daten der Tabelle. Der Überfahreinzustand wird von dem Kartenraster überwacht. Die internen Formfelder eines Modals werden von dem Bildschirm verwaltet, der das Modal auslöst. Jeder Tastendruck oder jede geringfügige Interaktion zwingt nun den übergeordneten Eigentümer dazu, neu zu rendern.
Weil das Seite-Component jedes Mal seine Kinderkomponenten aktualisiert, kann das Ändern des Suchtextes dazu führen, dass Diagramme, Tabellen, Navigationselemente und Dialogfelder erneut ausgeführt werden – obwohl sich ihre tatsächlichen Daten nicht geändert haben.
Auch in diesem Fall ist es nicht notwendig, jeden Bestandteil des States so tief wie möglich zu verbergen. Der State sollte an der kleinsten Grenze liegen, die noch zur Koordination der relevanten Komponenten erforderlich ist. Ein Filter, von dem URL, Tabelle und Export-Button abhängen, gehört vermutlich tatsächlich oberhalb dieser Bereiche. Ob derzeit ein einzelner Tooltip geöffnet ist, muss in der Regel nicht dort platziert werden.
Dieser Unterschied ist besonders wichtig bei Werten, die sich schnell ändern. Tippen, Ziehen, Pointer-Tracking, Animationsfortschritt sowie aktive Formulare können alle viele Aktualisierungen pro Sekunde erzeugen. Wenn man eines davon oberhalb eines ressourcenintensiven Unterbaums platziert, verwandelt sich eine rein lokale Interaktion in eine Aufgabe für die gesamte Seite.
Die State näher an die Komponenten zu bringen, die sie tatsächlich verwenden, führt in der Regel zu einer direkteren Verbesserung der Leistung als das Umhüllen der gesamten Seite mit Memoisierung, da React keinen Elternteil mehr hat, den man außer Kraft setzen könnte, sobald dieser nicht länger die betreffende State besitzt.
2. Verwendung eines Contexts für unzusammenhängende Werte
Context existiert, um ein spezifisches Problem zu lösen: Es ermöglicht Nachkomponenten, gemeinsame Werte abzurufen, ohne dass jede Zwischenkomponente Props weitergeben muss, die sie nie berührt.
Probleme entstehen, sobald ein einziger Provider zum Allzweckbehälter für alles wird, was als „global“ gilt.
Oft sieht man einen auf die gesamte Anwendung bezogenen Context, der den aktuellen Benutzer, das Theme, die Anzahl ungelesener Benachrichtigungen, die Sichtbarkeit der Seitenleiste, die Lokalisation, Feature-Flags, Berechtigungen sowie einige Aktionsfunktionen enthält. Das erscheint übersichtlich – ein Provider, ein benutzerdefinierter Hook zur Verwendung desselben.
Aber aus Sicht der Aktualisierungen teilen diese Werte keine einzige Verantwortung. Die Anzahl der Benachrichtigungen kann mehrmals pro Minute aktualisiert werden; das Theme kann monatelang unverändert bleiben. Die Sichtbarkeit des Sidebars ist ein lokaler UI-Zustand, während der authentifizierte Benutzer die Sitzung selbst widerspiegelt. Ihr Zusammenfassen bedeutet, dass jeder Verbraucher im Grunde für Änderungen abonniert wird, die ihm völlig egal sein können.
Oft baut der Anbieter seinen Kontextwert auch bei jeder Neuauflösung neu auf:
function AppProvider({ children }) {
const [theme, setTheme] = useState("light");
const [notifications, setNotifications] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(false);
const value = {
theme,
setTheme,
notifications,
setNotifications,
sidebarOpen,
setSidebarOpen,
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
Jedes Mal, wenn der Anbieter neu auflöst, wird value zu einem völlig neuen Objekt. Die Verbraucher betrachten dies als Änderung per Referenz, auch wenn das spezifische Feld, das sie lesen, tatsächlich nicht geändert wurde.
Durch das Speichern des dem Anbieter übergebenen Wertes können erneute Darstellungen, die durch Updates eines nicht verwandten Elternteils verursacht werden, verhindert werden, doch es ist nicht möglich, den Kontextwert in unabhängige Teile aufzuteilen. Wenn sich Benachrichtigungen ändern, gilt das gesamte kombinierte Objekt weiterhin als geändert.
Ein besseres Vorgehen besteht darin, den Kontext nach Verantwortungsbereichen und Aktualisierungsfrequenzen aufzuteilen, anstatt sich von der Bequemlichkeit einer einzigen Anbieterdatei leiten zu lassen. Konfigurationen, die selten ändern, können in einem Kontext gespeichert werden. Schnell wechselnde Live-Daten können ihren eigenen Kontext haben. Rein lokale Layout-Zustände benötigen möglicherweise überhaupt keinen Kontext auf Anwendungs-Ebene.
Kontext ist nicht nur ein Transportmechanismus für Werte – er dient auch zur Verteilung von Updates. Die Art und Weise, wie der Wert des Anbieters gestaltet wird, bestimmt, wie weit sich jede Änderung ausbreiten kann.
3. Wiederherstellung von Objekten, Arrays und Funktionen, wenn die Identität tatsächlich wichtig ist
Das Erstellen eines neuen Werts bei jeder Darstellung ist ein normales Verhalten von JavaScript. Ein inliner Objekt, ein gefilterter Array oder eine neue Pfeilfunktion kosten in der Regel so wenig, dass ein Versuch, sie zu vermeiden, keinen Vorteil bringt.
Es wird schwierig, sobald einige nachgelagerte Logiken von der Identität dieses Werts und nicht nur von seinem Inhalt abhängen.
Ein gememorisierter Kindkomponente prüft seine Props über die Referenz. Ein Effect prüft seine Abhängigkeitsliste ebenfalls über die Referenz. Eine Tabelle-Komponente könnte ihre internen Strukturen jedes Mal neu aufbauen, wenn sich die Referenz des Spalten-Arrays ändert. Ein Context Provider informiert die Abonnenten über eine Aktualisierung, sobald der von ihm gehaltene Wert auf eine neue Referenz verweist.
Zwei Objekte können identische Daten enthalten und dennoch im Speicher als zwei unterschiedliche Objekte existieren. Wenn eine Kindkomponente bei jeder Darstellung des Elternteils ein völlig neues Einstellungen-Objekt erhält, hat sie keine Möglichkeit zu erkennen, dass sich an diesen Einstellungen tatsächlich nichts geändert hat.
Eine gängige Reaktion ist es, standardmäßig jedes Objekt in useMemo und jede Funktion in useCallback zu packen. Das ersetzt lediglich eine reflexhafte Gewohnheit durch eine andere. Die Memoisierung ist nicht kostenlos – sie hat ihren eigenen Overhead, ihr eigenes Abhängigkeitsarray, das gepflegt werden muss, sowie ihre eigenen Wartungskosten. Sie lohnt sich nur dann, wenn eine stabile Referenz es ermöglicht, teure Operationen zu überspringen oder wiederverwenden.
Werte, die sich niemals ändern, können häufig ganz außerhalb der Komponente definiert werden. Anstatt mehrere Props aus reinen Gründen der Übersichtlichkeit in ein einziges Objekt zu packen, kann eine Kindkomponente oft direkt die einzelnen primitiven Werte erhalten. Eine Funktion sollte nur dann stabilisiert werden, wenn sie in einen Effect, eine Abonnementfunktion oder eine Memoisierungsgrenze eingespeist wird, wo die Identität tatsächlich das Verhalten bestimmt.
Die eigentliche Prüfung besteht darin, zu beobachten, was durch den neuen Referenzwert geschieht. Wenn sich nichts weiter davon auswirkt, ist es unbedenklich, den Wert bei jeder Neuansicht neu zu berechnen. Doch wenn ein neues Objekt dazu führt, dass eine große Tabelle neu erstellt werden muss oder eine Abonnementverbindung abgebrochen und wieder hergestellt wird, dann ist die Identität dieses Objekts zu einem Bestandteil des Leistungsvertrags der Komponente geworden und sollte entsprechend behandelt werden.
4. Berechnete Werte speichern und über Effects aktualisieren
Eine der am wenigsten offensichtlichen Ursachen für zusätzliche Darstellungen ist das Beibehalten eines Zustandswerts für etwas, das einfach aus vorhandenen Props oder Zuständen berechnet werden könnte.
Betrachten Sie ein Komponente, das die vollständige Liste der Datensätze sowie den aktuellen Filtertext enthält und außerdem eine separat gespeicherte, gefilterte Liste bereitstellt. Oder ein Formular, das seine Felder und Validierungsfehler verfolgt sowie außerdem ein eigenständiges canSubmit-Flag. Oder eine Liste-Komponente, die sowohl eine ausgewählte ID als auch einen zweiten Zustandswert speichert, der das entsprechende ausgewählte Objekt enthält.
Weil dieser doppelte Wert aus dem Gleichgewicht geraten kann, fügen Entwickler einen Effect hinzu, um ihn auf dem neuesten Stand zu halten:
function Products({ products }) {
const [search, setSearch] = useState("");
const [visibleProducts, setVisibleProducts] = useState(products);
useEffect(() => {
setVisibleProducts(
products.filter((product) =>
product.name.toLowerCase().includes(search.toLowerCase())
)
);
}, [products, search]);
// ...
}
So verhält es sich, wenn sich search ändert: Zunächst wird die Komponente mit der veralteten, noch im Zustand vorhandenen gefilterten Liste gerendert, React speichert diese Darstellung ab, anschließend wird der Effect ausgelöst, der den Zustand aktualisiert und eine weitere Renderung ansetzt.
Diese zweite Renderung existiert nur, weil ein berechnetes Ergebnis so gespeichert wurde, als wäre es ein eigenständiger Zustandswert.
Falls visibleProducts stattdessen direkt während der Darstellung berechnet wird, bleibt es automatisch synchron und die zusätzliche Darstellung verschwindet vollständig. Wenn die Filterlogik besonders aufwendig ist, kann man die Berechnung in eine Memoisierung einbetten, die nach ihren tatsächlichen Eingaben sortiert ist. Wichtig ist, dass diese Art der Memoisierung nur eine Berechnung speichert – sie sollte niemals das gespeicherte Ergebnis in eine zweite, unabhängig schreibbare Quelle der Wahrheit verwandeln.
Dieses Muster hat Folgen jenseits von verschwendeten Darstellungen. Abgeleiteter Zustand kann kurzzeitig einen Wert anzeigen, der seinen Eingaben widerspricht, und das Stapeln mehrerer Effects kann zu einer Kette von wiederholten Darstellungen führen. Durch Beseitigung des überflüssigen Zustands werden in der Regel auch mehrere Synchronisierungs-Effects beseitigt.
5. Abonnieren von mehr Daten aus einem externen Speicher, als man tatsächlich benötigt
Selektorengesteuerte Zustandsbibliotheken können sehr gezielte Aktualisierungen liefern – aber nur, wenn die Komponenten tatsächlich enge, spezifische Werte auswählen.
Ein häufiger Abkürzungsweg besteht darin, sich einem gesamten Store oder einem großen Objekt auf Ebene einer Funktion anzumelden und anschließend nur ein einzelnes Feld zur Verwendung auszuwählen. Dadurch wird die Komponente jedes Mal neu gerendert, wenn sich etwas innerhalb dieses umfassenderen Objekts ändert – sogar Felder, die sie nie berührt.
Dieses Problem ist nicht auf Zustandsverwaltungsbibliotheken beschränkt. Es tritt genauso bei Caches zur Datenerfassung, systems basierend auf Observables sowie selbst erstellten Abonnementsmechanismen auf. Das Lesen aus einer großen Quelle allein aus Bequemlichkeit sorgt dafür, dass eine Komponente auf weitaus mehr Änderungen reagiert, als nötig wäre.
Fraglich ist nicht nur, wie viel Zustand eine Komponente technisch gesehen erreichen darf, sondern auch, welcher Teil dieses Zustands zur Auslösung ihrer Neurenderung zulässig sein sollte.
6. Instabile Schlüssel, die stillschweigend Neuinstallationen statt Aktualisierungen verursachen
Eine erneute Darstellung führt eine Komponente erneut aus, wobei ihre Identität, ihr lokaler Zustand und ihre Position unverändert bleiben. Eine Neuinstallation hingegen handelt anders – sie zerstört die vorhandene Komponenteninstanz und erstellt eine völlig neue von Grund auf. Neuinstallationen sind in der Regel deutlich aufwändiger, da React den vorherigen lokalen Zustand, die bestehenden DOM-Beziehungen oder Ressourcen, die mit dem Lebenszyklus der Komponente verbunden sind, nicht mehr wiederverwenden kann.
Schlüssel, die nicht stabil sind, können das, was eigentlich einfache Aktualisierungen sein sollten, stillschweigend in wiederholte Neuinstallationen verwandeln.
Falls ein Schlüssel aus einem zufälligen Wert, dem aktuellen Zeitstempel oder einem Feld abgeleitet wird, das während der Bearbeitung des Elements geändert wird, interpretiert React dies als vollständiges Verschwinden des alten Elements. Da jede Darstellung eine andere Identität erzeugt, wird das vorherige Kind entfernt und an seiner Stelle ein völlig neues montiert.
Oft treten die sichtbaren Symptome bereits auf, bevor jemand überhaupt ein Leistungsproblem bemerkt. Texteingaben verlieren während des Eintippens den Fokus, erweiterte Zeilen schließen sich wieder, Animationen starten von vorne und der Zustand lokaler Komponenten verschwindet. Die Leistungsbeeinträchtigung wird ernst, wenn die neu geladenen Komponenten aufwändigere Initialisierungsarbeiten durchführen müssen – beispielsweise Editor, Diagramme, Observer, Abonnements, Mediaplayer oder Netzwerkanfragen initialisieren.
Das Verwenden von Array-Indizes als Schlüssel führt zu einer subtileren Variante dieses Problems, sobald Listeinträge umsortiert, eingefügt oder entfernt werden können. React kann den Zustand einer Komponente weiterhin mit einer bestimmten Position in der Liste verknüpfen, obwohl nun ein anderer Eintrag diese Position einnimmt. Dieser Unterschied kann dazu führen, dass der Zustand dem falschen Eintrag zugeordnet wird und unnötige Abgleichsarbeiten erforderlich werden – alles weil die Schlüssel nicht tatsächlich stabile Identitäten der Einträge widerspiegeln.
Eine gut gewählte Schlüsselwerte sollte die dauerhafte Identität eines Elements in seiner Liste darstellen. Sie sollte nur dann geändert werden, wenn dieses Element als völlig unterschiedliche Instanz behandelt werden soll – und nicht einfach deshalb, weil zufällig eine Neuzeichnung stattgefunden hat.
Es gibt berechtigte Fälle, in denen das bewusste Ändern eines Schlüsselwerts das richtige Mittel ist, da dadurch React gezwungen wird, einen ganzen Unterbaum zurückzusetzen. Das sollte eine bewusste Entscheidung sein, denn dadurch wird alles, was React zuvor für diesen Unterbaum gespeichert hatte, verworfen. Eine unbeabsichtigte Neuinstallation ist weitaus aufwendiger als eine normale Neuzeichnung, und es ist leicht, sie zu übersehen, wenn der Schlüsselausdruck auf den ersten Blick unbedenklich erscheint.
7. Schwere Benutzeroberflächen weiterhin im Betrieb lassen, wenn sie außer Sichtweite sind
Ein Komponente muss nicht sichtbar sein, um weiterhin zu funktionieren.
Tabs, Schubladen, Modale und zusammenfaltbare Panels werden häufig weiterhin eingebunden gelassen, aber mit CSS visuell versteckt, da dieser Ansatz den internen Zustand bewahrt und es der Benutzeroberfläche ermöglicht, sofort wieder geöffnet zu werden. Für leichtes Content ist dieses Kompromiss oft vertretbar.
Aber bei größeren Benutzeroberflächen bedeutet das Weiterhin-Eingebunden-Sein, dass sie weiterhin auf den Kontext und externe Speicher lauschen, neue Eigenschaften erhalten, weiterhin neu rendern, Berechnungen durchführen und ihre Effekte am Laufen halten – während der Benutzer keinerlei Möglichkeit hat, das Ergebnis zu sehen. Eine Kontrollleiste könnte mehrere Diagramme vollständig aktiv hinter Tabs halten, auf die derzeit niemand schaut. Ein Editor in einem versteckten Panel könnte weiterhin Hintergrundprozesse zur Verarbeitung von Dokumentänderungen durchführen. Ein visuell geschlossenes Dialogfeld kann weiterhin einen umfangreichen Formbaum samt seiner Validierungslogik beibehalten.
Ihre Anwendung zahlt dafür einen echten Preis, denn React betrachtet den montierten Unterbaum weiterhin als Teil der relevanten Render-Pfade. Die Einstellung display: none ändert lediglich die Darstellung im Browser – sie hat keinen Einfluss darauf, ob React weiterhin diesen Unterbaum besitzt und aktualisiert.
Trotzdem ist das Entmontieren jedes versteckten UI-Teils auch nicht automatisch die richtige Entscheidung. Jedes Mal, wenn jemand zu einer anderen Registerkarte wechselt, einen aufwändigen Editor von Grund auf neu aufzubauen, kann insgesamt langsamer sein und den Benutzerzustand löschen, der eigentlich erhalten bleiben sollte. Entscheidend ist es, eine bewusste Wahl auf der Grundlage des Lebenszyklus des Komponenten sowie dessen tatsächlicher Kosten zu treffen.
Eine Möglichkeit besteht darin, das Einbinden eines schweren Panels bis zu seiner ersten Aktivierung hinauszuzögern. Elemente, die tatsächlich einer visuellen Entbindung standhalten müssen, können in einem kleineren, speziell dafür vorgesehenen Bereich bleiben, während der dargestellte Baum selbst entfernt wird. Einige Produkte finden einen Mittelweg, indem sie nur die kürzlich am meisten genutzten Panels eingebunden lassen und den Rest freigeben.
Die eigentliche Frage ist nicht, ob etwas derzeit auf dem Bildschirm ist. Es geht darum, ob es auch dann weiterhin Updates erhalten und darauf reagieren sollte, wenn es außerhalb des Bildschirms ist.
8. Erlauben, dass Effekte aufgrund instabiler Abhängigkeiten neu gestartet werden
Ein Effekt, der von einem bestimmten Wert abhängt, wird sich jedes Mal aufräumen und neu ausführen, wenn sich dieser Wert ändert – und das ist genau richtig, wenn die externe Beziehung, die der Effekt darstellt, tatsächlich geändert hat.
Das Problem ist, dass instabile Abhängigkeiten den Eindruck erwecken können, als hätte sich diese Beziehung bei jeder Renderung geändert – obwohl eigentlich nichts Bedeutendes geschehen ist.
Betrachten Sie ein Komponente, das ein Options-Objekt erstellt und es an einen benutzerdefinierten Hook weitergibt, der dieses Objekt anschließend als Abhängigkeit für einen Effect verwendet. Da der Elternteil bei jeder Renderung ein völlig neues Objekt erstellt, zerstört der Effect ständig seine Verbindung und stellt sie wieder her. Ebenso wird ein Event-Handler, der bei jeder Renderung neu erstellt und als Abhängigkeit für einen Listener-Effect verwendet wird, dazu führen, dass dieser Listener immer wieder entfernt und neu angehängt wird. Ein Abfrage-Konfigurationsobjekt kann jedes Mal eine neue Anfrage auslösen, obwohl sich tatsächlich keine seiner bedeutungsvollen Felder geändert hat.
Diese Art von Overhead zeigt sich nicht immer als Rendering-Problem. Stattdessen treten doppelte Netzwerkanfragen, schnelles Öffnen und Schließen von Verbindungen, flackernde Benutzeroberflächen oder Ereignislistener auf, die verschwinden und wieder auftauchen. Noch schlimmer ist, dass diese Neustarts der Effekte oft selbst State-Setter aufrufen, was weitere Renderungen auslöst und die ursprüngliche Ursache des Problems verschleiert.
Die Lösung besteht nicht darin, Abhängigkeiten aus dem Array zu entfernen, nur um die Warnungen zum Schweigen zu bringen. Abhängigkeiten dienen dazu, genau anzugeben, welche Werte der Effekt liest – ihr Verbergen führt zu veralteten Schließfunktionen und gestörter Synchronisierung.
Der bessere Ansatz ist es, der Abhängigkeit eine stabile Identität zu geben, wenn genau diese Identität wirklich wichtig ist. Je nach Situation können Sie das Objekt direkt innerhalb des Effect aus primitiven Werten erstellen, den Callback stabilisieren, damit er nicht bei jeder Darstellung neu erstellt wird, oder erkennen, dass die Logik überhaupt nicht in einen Effect gehört, und sie stattdessen in einen Ereignishandler verschieben.
Ein Effect sollte neu gestartet werden, weil das externe System tatsächlich eine neue Synchronisierung erfordert – und nicht, weil bei einer Darstellung zufällig ein weiteres funktional identisches Objekt erzeugt wurde.
9. Zustand durch ein neues Objekt ersetzen, das dasselbe darstellt
Abschließend hängt es davon ab, wie React Werte vergleicht: Manchmal überspringt es eine Neuzeichnung, wenn der nächste Zustand dem aktuellen entspricht. Bei Objekten und Arrays sorgt es jedoch dafür, dass der Zustand als unterschiedlich erscheint, selbst wenn sein tatsächlicher Inhalt derselbe ist, indem man React eine neue Referenz zur Verfügung stellt.
Dieses Muster tritt häufig in Event-Handlern und Store-Logik auf. Ein Filter wird auf ein völlig neues Objekt zurückgesetzt, das eigentlich dem bereits vorhandenen entspricht. Ein Reducer gibt für eine Aktion, die nichts geändert hat, ein neu erstelltes Zustandsobjekt zurück. Eine Antwort aus dem Polling wird in ein neues Array kopiert, obwohl jeder Eintrag darin unverändert bleibt.
React verfügt nicht über eine allgemeine Methode, um selbst tiefgreifende semantische Gleichheit zu erkennen. Wenn der Zustandseigentümer ein neues Objekt zurückgibt, betrachtet React dies als eine ausreichend bedeutende Aktualisierung, die verarbeitet werden muss.
Um dies zu vermeiden, sollten Reducer das vorhandene State-Objekt zurückgeben, wenn eine Aktion tatsächlich nichts ändern würde. State-Setter können die relevanten Werte vergleichen, bevor sie ein ganzes strukturiertes Objekt ersetzen. Schichten, die für die Normalisierung eingehender Daten zuständig sind, können bei unveränderten Datensätzen dieselben Referenzen beibehalten, anstatt nach jeder Antwort die gesamte Sammlung von Grund auf neu zu erstellen.
Das bedeutet jedoch nicht, dass Sie jede State-Änderung gründlich vergleichen sollten. Ein solcher tiefgehender Vergleich kann letztendlich teurer sein als das Rendering, das er verhindern soll, und er führt zu einer weiteren Vorstellung von „Gleichheit“, die nun konsequent beibehalten werden muss. Ein nachhaltigerer Ansatz besteht darin, die Objektidentität natürlich beizubehalten, sobald sich die zugrunde liegenden Daten tatsächlich nicht geändert haben.
Strukturelles Teilen ermöglicht es React sowie allen darunterliegenden, gememorierten Verarbeitern, einen echten Wandel von einer wiederholten Kopie derselben Informationen zu unterscheiden. Eine neue Referenz sollte auf einen neuen Zustand hindeuten – und nicht nur auf eine neue Speicherzuweisung mit denselben Werten.
Ziel ist es nicht, erneute Darstellungen zu beseitigen
React-Apps werden nicht schneller, weil sie sich weigern, etwas darzustellen. Sie werden schneller, weil die Darstellungen bedeutungsvolle Änderungen erkennen und sich auf die Teile des Baums beschränken, die tatsächlich ihre Ausgabe neu überdenken müssen.
Das Neuerstellen einer kleinen, einfachen Komponente ist oft günstiger als das Erstellen und Warten eines aufwendigen Memoisierungsschemas dafür. Ein Kontext-Update, das nur wenige leichte Verarbeiter betrifft, rechtfertigt möglicherweise überhaupt keine architektonischen Anpassungen. Entscheidungen bezüglich der Leistung sollten anhand des tatsächlichen Kostenfaktors der Interaktion, durch Profilierung sowie anhand der tatsächlich erwarteten Skala getroffen werden – und nicht aus dem allgemeinen Instinkt heraus, die Darstellung überall möglichst zu vermeiden.
Die oben beschriebenen Gewohnheiten sind gerade deshalb riskant, weil sie stillschweigend den Umfang dessen erweitern, was aktualisiert wird. Das Zu hoch setzen des Zustands verwandelt eine kleine, lokale Interaktion in eine Aufgabe, die die gesamte Seite betrifft. Ein breiter Kontext sowie Abonnements an Speichern führen dazu, dass unverwandte Änderungen an Komponenten übertragen werden, die davon nichts wissen müssen. Frisch erstellte Referenzen lassen stabile Eingaben von einer Darstellung zur nächsten anders aussehen. Abgeleiteter Zustand in Kombination mit Effects kann Kettenreaktionen weiterer Aktualisierungen auslösen. Instabile Schlüssel verhindern die Wiederverwendung und zwingen stattdessen zu vollständigen Neuladungen. Versteckte Unterbäume führen weiterhin Arbeit aus, die niemand sehen kann, und Zustände, die strukturell identisch sind, werden weiterhin als völlig neu behandelt.
Memoisierung kann tatsächlich helfen, sobald diese zugrundeliegenden Beziehungen richtig verstanden wurden. Sie funktioniert am besten, wenn die Grenzen der Komponenten, das Eigentumsverhältnis am Zustand sowie die Referenzidentitäten bereits für sich genommen sinnvoll sind. Ohne diese Grundlage wird die Memoisierung lediglich zu einer Schutzschicht um einen Update-Graphen, der dennoch unnötig groß bleibt.
Die nützlichste Frage zur Leistung von React lautet nicht „Wie verhindere ich, dass diese Komponente gerendert wird?“
Sondern „Was hat sich tatsächlich geändert, wer trägt diese Änderung vor und warum wurde von dieser Komponente erneut verlangt, sich selbst zu überprüfen?“
Sobald man klare Antworten auf diese Fragen hat, verschwinden in der Regel viele unnötige Neu-Renderungen von selbst – ohne dass der Komponentenbaum zu einem Gewirr aus Caches und Vergleichsfunktionen wird.