Erledigen Sie zuerst weniger Arbeit: Eine Leistungsüberprülliste für React vor der Memoisierung
Senken Sie die Kosten einer React-App, indem Sie Arbeit vermeiden: Debouncing für Suchfunktionen anwenden, Seitenbereichung nutzen, Zustände gemeinsam platzieren, stabile Schlüssel verwenden, die Verarbeitung auf der Serverseite durchführen und Inhalte verspätet laden – bei Bedarf anschließend mit Memoisierung arbeiten.
Wenn eine React-Anwendung langsam wirkt, ist die erste Reaktion, zu useMemo, useCallback oder React.memo zu greifen. Diese Werkzeuge machen bereits vorhandene Aufgaben effizienter, doch in vielen Anwendungen entsteht der eigentliche Aufwand durch Arbeiten, die überhaupt nicht notwendig wären: redundante Anfragen, zu große Datenmengen, Zustandsänderungen, die sich auf einen großen Teil der Anwendung auswirken, sowie Code, den die Benutzer noch nicht einmal angefordert haben. Dieser Leitfaden zeigt sieben Möglichkeiten auf, unnötige Arbeiten zu beseitigen, bevor man das Verbleibende optimiert – mit einer Checkliste, die bei Code-Reviews angewendet werden kann.
Die zentrale Frage bleibt stets einfach: Kann diese Arbeit ganz vermieden werden?
Weniger Anfragen senden: Sucheingabe deaktivieren
Suchfelder sind eine klassische Quelle für verschwendete Anfragen. Ein naiver Handler ruft die API bei jeder Änderung auf:
const handleSearch = (value) => {
fetchUsers(value);
};
Das Eingeben des Wortes „React“ in dieses Feld erzeugt eine Anfrage pro Tastendruck:
R
Re
Rea
Reac
React
Fünf Hin- und Rückreisen für eine Suche, wobei vier davon Ergebnisse liefern, die niemand ansehen wird. Ein besseres Vorgehen besteht darin, zu warten, bis der Benutzer kurz pausiert, und erst dann die Anfrage zu senden. Genau das macht Debouncing: Jeder neue Aufruf setzt einen Timer zurück, und die umschlossene Funktion wird nur ausgeführt, wenn der Timer ununterbrochen abgelaufen ist.
const handleSearch = debounce((value) => {
fetchUsers(value);
}, 300);
Mit einem Zeitfenster von 300 ms lösen schnelle Tippenden am Ende nur eine einzige Anfrage aus. Dadurch wird der Netzwerkverkehr, die Serverlast sowie die Verarbeitung von verworfenen Antworten auf der Client-Seite eingespart. Beachten Sie, dass hier nichts mit dem Rendering zu tun hat; der Vorteil entsteht ausschließlich dadurch, dass die Arbeit nicht erledigt werden muss.
Eine Einschränkung bei der Verwendung eines solchen Hilfsfunktionen innerhalb einer Komponente: Wenn debounce(...) direkt im Komponentenkörper aufgerufen wird, wird bei jeder Neuzeichnung eine neue degebouncede Funktion (und ein neuer Timer) erstellt, was die Debouncing-Funktion beeinträchtigt. Erstellen Sie sie daher einmal – beispielsweise mit useMemo oder useRef – oder verwenden Sie das unten beschriebene Muster basierend auf Effects.
Eine eigenständige, degebouncede Suchkomponente
Dieselbe Idee kann auch ausschließlich mit React-Primitiven ohne Hilfsbibliotheken umgesetzt werden. Beginnen Sie mit den Imports sowie zwei Zuständen: dem aktuellen Eingabetext und den heruntergeladenen Benutzern.
import { useEffect, useState } from "react";
function UserSearch() {
const [search, setSearch] = useState("");
const [users, setUsers] = useState([]);
Ein Effect wird jedes Mal ausgeführt, wenn sich search ändert. Anstatt sofort Daten herunterzuladen, wird die Ausführung mit setTimeout verschoben. Im Callback führt eine leere oder nur aus Leerzeichen bestehende Abfrage dazu, dass die Ergebnisse gelöscht werden und der Vorgang ohne Anfrage früh beendet wird.
useEffect(() => {
const timer = setTimeout(async () => {
if (!search.trim()) {
setUsers([]);
return;
}
Bei einer echten Abfrage fordert die Callback-Funktion passende Benutzer an, wobei der Suchbegriff kodiert wird, damit Sonderzeichen die URL nicht stören:
const response = await fetch(
`/api/users?search=${encodeURIComponent(search)}`
);
Anschließend wird das JSON analysiert und das Ergebnis gespeichert, alles noch innerhalb des 300-Millisekunden-Timers:
const data = await response.json();
setUsers(data);
}, 300);
In der Aufräumfunktion findet tatsächlich die Verzögerungstechnik statt. React ruft sie vor dem erneuten Ausführen des Effects auf, sodass jede Tastenbetätigung den vorherigen wartenden Timer stört:
return () => clearTimeout(timer);
}, [search]);
Schließlich rendernt das Komponente ein kontrolliertes Eingabefeld, das mit search verknüpft ist:
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search users..."
/>
Und es listet die Benutzer unter Verwendung ihrer IDs auf:
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
Jeder eingegebene Zeichen setzt den Timer zurück, und die Anfrage wird erst nach 300 ms Stille abgesendet. Bedenken Sie, dass das Debouncing die Anzahl der gesendeten Anfragen verringert, aber nicht garantiert, dass sie in der richtigen Reihenfolge abgeschlossen werden; eine langsame frühere Antwort kann dennoch eine neuere überschreiben. Wenn das für Ihre Benutzeroberfläche wichtig ist, kombinieren Sie dies mit der Stornierung von Anfragen, wie in „Fixing race conditions: Debouncing kann in Such-Benutzeroberflächen nicht helfen“ beschrieben.
Die allgemeine Lektion: Die Verhinderung von Arbeit ist in der Regel besser als die Beschleunigung bereits vorhandener Arbeit.
Laden Sie nur die Daten, die der Bildschirm benötigt
Eine weitere häufige Ursache für Ressourcenverbrauch ist das Herunterladen von weitaus mehr Daten, als angezeigt wird. Angenommen, ein Endpunkt gibt 10.000 Benutzer an, während die Ansicht jeweils nur 20 zeigt. Der Browser muss trotzdem alle Daten herunterladen, verarbeiten und im Speicher halten, und React muss mit viel größeren Arrays arbeiten, als notwendig wäre.
Soweit der Anwendungszweck es zulässt, nutzen Sie Paginierung, sodass jede Anfrage nur eine Seite enthält:
API
↓
20 users
↓
Browser
↓
Display
Jedes Mal, wenn der Benutzer weiterblättert, wird der nächste Datensatz angefordert. Dadurch wird die Netzwerknutzung, der Speicherverbrauch, die Verarbeitung auf der Client-Seite sowie das Datenvolumen, das durch Ihre Komponenten fließt, reduziert. Infinite Scroll und cursorbasierte APIs folgen demselben Prinzip. Die Änderung der Datenabfragemethode hat oft einen weitaus größeren Einfluss als die Optimierung einer einzelnen Komponente.
Bewahren Sie stark veränderliche Zustände in der Nähe ihrer Verarbeiter auf
Die Stelle, an der sich der Zustand befindet, bestimmt, wie viel des Baums neu gerendert wird, wenn sich dieser ändert. Betrachten wir ein Dashboard, das den Suchtext verwaltet:
function Dashboard() {
const [search, setSearch] = useState("");
return (
<>
<SearchBox value={search} onChange={setSearch} />
<Analytics />
<UserTable />
</>
);
}
Jeder Tastendruck aktualisiert Dashboard, wodurch auch Analytics und UserTable neu gerendert werden, obwohl keines dieser Komponenten den Suchwert verwendet. Bei einem großen Dashboard summieren sich diese Effekte. Wenn der Zustand nur für einen kleinen Teil der Benutzeroberfläche relevant ist, führt seine Verlegung in diesen Teil (hier direkt in SearchBox) dazu, dass Updates nur dort stattfinden, wo sie benötigt werden, und die Struktur der Komponenten wird übersichtlicher.
Es handelt sich dabei nicht um eine Regel, den Zustand immer nach unten zu verlagern. Wenn mehrere Komponenten tatsächlich denselben Wert benötigen, ist ihr nächster gemeinsamer Elternteil der richtige Eigentümer. Ziel ist es, den Zustand auf dem niedrigsten Niveau zu halten, das dennoch von jeder Komponente gelesen werden kann, die ihn tatsächlich benötigt.
Geben Sie dynamischen Listeneinträgen stabile Schlüssel
Listen sind Orte, an denen kleine Details überproportional große Auswirkungen haben. Ein gängiges Muster verwendet den Array-Index als Schlüssel:
{users.map((user, index) => (
<UserCard key={index} user={user} />
))}
Index-Schlüssel sind nicht immer falsch. Für eine statische Liste, deren Reihenfolge sich nie ändert, funktionieren sie. Bei einer dynamischen Liste gibt ein stabiles ID aus den Daten jedem Element eine dauerhafte Identität:
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
Der Unterschied liegt darin, was der Schlüssel repräsentiert:
index → position
id → identity
Falls Elemente hinzugefügt, entfernt, umgeordnet oder gefiltert werden können, verschieben sich die Positionen und React vergleicht falsche Elemente: Es kann mehrfach als nötig neu rendern, und schlimmer noch, es kann den internen Zustand eines Elements mit dem eines anderen verknüpfen. Das wird zu einem sichtbaren Fehler, wenn die Listenelemente Eingabefelder, lokalen Zustand oder andere interaktive Elemente enthalten – beispielsweise ein Textfeld, das seinen eingegebenen Wert behält, während die Zeile darunter sich ändert. Wählen Sie immer ein stabiles ID, wenn sich die Liste ändern kann.
Fragen Sie nach der Verarbeitung, nicht nur nach ihrer Geschwindigkeit
Umfangreiche Transformationen auf der Client-Seite sind eine weitere Quelle für vermeidbare Kosten. Dabei werden Benutzer gefiltert, sortiert und in neue Objekte umgewandelt:
const filteredUsers = users
.filter((user) => user.isActive)
.sort((a, b) => a.name.localeCompare(b.name))
.map((user) => ({
...user,
displayName: user.name.toUpperCase()
}));
Bei einigen tausend Datensätzen wird das Wiederholen dieser Vorgänge bei jeder Darstellung teuer. Die Speicherung in einem Cache ist eine Möglichkeit, doch zuerst sollte geprüft werden, ob der Client diese Arbeit überhaupt durchführen muss. Oft kann die Endpunkt-Instanz bereits aktivierte Benutzer filtern, die Datenbank kann die Sortierung übernehmen, wobei Indizes dies kostengünstig machen, und die Paginierung kann den Datensatz verkleinern, sodass die verbleibende Verarbeitung unbedeutend ist. Die Reduzierung der Eingabedaten ist in der Regel effektiver als die Beschleunigung der Berechnungen.
Funktionen auf Anfrage laden
Große Anwendungen enthalten Funktionen, die viele Benutzer in einer bestimmten Sitzung nie öffnen. Eine Berichtsseite zum Beispiel kann völlig getrennt vom Haupt-Dashboard sein. Anstatt sie im initialen Download mitzuliefern, sollte sie verspätet geladen werden:
const Reports = lazy(() => import("./Reports"));
Mit React.lazy wird die Datei des Moduls zum ersten Mal geladen, wenn der Komponente rendernt – und das muss innerhalb eines Suspense-Bereichs geschehen, der während des Ladevorgangs eine Ersatzanzeige zeigt. Dies bringt vor allem Vorteile in Apps mit vielen Routen, großen Funktionsbereichen, schweren Abhängigkeiten wie Diagramm- oder Editor-Bibliotheken oder Seiten, die nur von wenigen Nutzern aufgerufen werden.
Sie sollten klar wissen, was Sie dadurch gewinnen: eine kleinere anfängliche JavaScript-Größe sowie eine schnellere erste Ladezeit. Der Code innerhalb der lazy-Komponente läuft nach dem Laden nicht schneller, und es entsteht eine kurze Ladeverzögerung beim ersten Öffnen der Funktion.
Memoisieren mit Grund
Ein häufiger Fehler besteht darin, Optimierungs-APIs standardmäßig über den gesamten Codebase zu verteilen – beispielsweise indem jeder Handler umhüllt wird:
const handleClick = useCallback(() => {
setSelectedUser(id);
}, [id]);
Oder jeder abgeleitete Wert:
const data = useMemo(() => {
return processData(users);
}, [users]);
Diese Hooks haben legitime Anwendungsfälle, doch jeder von ihnen fügt Code hinzu, Abhängigkeitslisten, die korrekt gehalten werden müssen, sowie einen eigenen geringen Laufzeitkostenanteil. Bevor man einen hinzufügt, prüfen Sie:
- Ist die Berechnung tatsächlich aufwendig?
- Wird sie häufig ausgeführt?
- Wird das Ergebnis bei mehreren Darstellungen wiederverwendet?
- Hängt ein gememorisierter Kindkomponente oder ein Effect von einer stabilen Referenz ab?
- Wird die Änderung einen messbaren Unterschied bewirken?
Falls die Antworten größtenteils „Nein“ lauten, ist der einfachere Code der bessere Code. Die Leistung entsteht durch die richtige Optimierung am richtigen Ort – nicht durch die Menge an Optimierungscode. Der React Compiler, sofern er verwendet wird, automatisiert einen Großteil dieser Memoisierung, was ein weiterer Grund ist, sie nicht überall von Hand zu schreiben; siehe was der React Compiler optimiert und was er übrig lässt.
Überprüfungsliste
Gehen Sie bei der Überprüfung einer React-Funktion diese Fragen durch:
- Unnötige Anfragen? Achten Sie auf Aufrufe pro Tastendruck, doppelte Anfragen sowie Abfragen nach Daten, die derzeit nicht angezeigt werden.
- Zu viele Daten? Nutzen Sie Paginierung, Filtern auf dem Server und verschieben Sie Abfragen auf den Zeitpunkt, an dem die Daten benötigt werden.
useMemo, useCallback oder React.memo nicht einfach nur deshalb hinzu, weil sie vorhanden sind.Leistung ist ein Prozess, keine einzelne Renderung
Die Leistung von React betrifft nicht nur React selbst. Kosten entstehen entlang der gesamten Arbeitskette:
API calls
↓
Amount of data
↓
State updates
↓
Component structure
↓
Data processing
↓
Rendering
↓
Bundle size
Wenn man sich nur auf die Render-Schicht konzentriert, kann dadurch ein weitaus größeres Problem weiter oben in der Kette übersehen werden. Das Beschleunigen des Renderings um Millisekunden nützt wenig, wenn die Seite weiterhin überflüssige Anfragen sendet, und das Merken von Komponenten hilft nicht, wenn Tausende von Datensätzen heruntergeladen werden, die nie angezeigt werden. Man sollte bei der Lösung von oben anfangen und sich nach unten arbeiten.
Kernpunkte
- Das Weglassen von Aufgaben (Anfragen, Bytes, Aktualisierungen, Code) bringt in der Regel mehr als das Beschleunigen solcher Aufgaben.
- Debouncen Sie anfragengetriebene Anfragen und kombinieren Sie dies mit der Stornierung, wenn Reihenfolge wichtig ist.
- Lassen Sie den Server filtern, sortieren und paginieren; senden Sie dem Client nur das, was angezeigt wird.
- Platzieren Sie den Zustand auf der niedrigsten Ebene, die allen Lesern dient, und kennzeichnen Sie dynamische Listen nach ihrer Identität.
- Verwenden Sie Lazy Loading für eine leichtere erste Ladezeit und greifen Sie nur dann auf Memoisierung zurück, wenn ein konkreter Nutzen dies rechtfertigt.
Verwandte Artikel
- Was der React Compiler optimiert und was er auf Ihren Platz lässt — Erfahren Sie, welche Leistungsverbesserungen der React Compiler automatisiert, warum langsame APIs und große Bundle weiterhin Ihre Aufgabe sind, und wie Sie ihn sicher in einer bestehenden React-Codebasis einsetzen können.
- Diagnose von React-Leistungsproblemen jenseits der API-Antwortzeit — Ermitteln Sie, warum schnelle APIs keine schnellen UIs garantieren, und wie Rendering, Bundle-Größe sowie Dateiorganisation heimlich die tatsächliche Leistung einer React-Anwendung beeinflussen.
- Die Seite responsiv halten: Wann CPU-Arbeit an einen Web Worker auslagern — Erfahren Sie, warum schwere Schleifen die Browser-Oberfläche einfrieren, wie das Web Worker-Messaging-Modell diese Arbeit ablädt und wie Sie entscheiden können, ob eine Aufgabe überhaupt einen Worker benötigt.
- Leistungsbudgets für JavaScript: Einhaltung von Bundle-Grenzen in CI — Erfahren Sie, warum JavaScript weitaus mehr kostet als seine Ladezeit, wie man ein realistisches Leistungsbudget definiert und wie man mit webpack Builds verhindern kann, die dieses Budget überschreiten.
- Lesbarer JavaScript am Beispiel: Zehn Refaktorisierungen vor und nach der Umsetzung — Zehn kleine Refaktorisierungen in JavaScript und React, von Namensgebung und Parameterobjekten über Fehlerbehandlung bis hin zur Formatierung, die es dem nächsten Entwickler erleichtern, den Code zu lesen und zu ändern.
- Warum React.lazy() eine Standardexportfunktion benötigt und wie man benannte Exporte lädt — Erschließen Sie, was React.lazy() von einer dynamischen Importierung erhält, warum benannte Exporte dies stören, wie man sie auf Standardexporte umleitet und wie Suspense sowie Code-Splitting dazu passen.
- Design von Offline-First-Frontends als Replikate: Warteschlangen, Wiederholungsversuche, Konflikte — Erfahren Sie, warum das Offline-First-Konzept den Browser in eine Datereplikate umwandelt, und wie lokale Speicher, langlebige Mutationswarteschlangen, idempotente Wiederholungsversuche sowie Konfliktregeln zusammenwirken.