Zum Inhalt
wannacode

Geschwindigkeit: wann eine Komponente neu rendert

Kurz gesagt: In React rendert eine Komponente neu, wenn sich ihr Zustand oder ihr Elternelement ändert; du misst zuerst, dann stoppen memo, useMemo und useCallback die unnötigen Renderings.

Problem

Jeder Buchstabe rendert die ganze Liste

Oben gibt es ein Suchfeld und einen Button „Suchen“. Die Liste wird erst nach „Suchen“ gefiltert. ProductList schreibt bei jedem Rendern in die Konsole. Tippe „milch“ ins Feld und sieh in die Konsole.

React

Fünf Buchstaben, fünf neue Zeilen „rendere ProductList“. Die Liste ist dieselbe: Du hast noch nicht auf „Suchen“ geklickt. Hier gibt es acht Produkte, und du merkst es nicht. In einem echten Katalog sind es 2000, jedes mit Bild und Preis, und das Feld beginnt, deinen Fingern hinterherzuhinken.

Die Idee

Wann eine Komponente neu rendert

Eine Komponente rendert neu, wenn sich die Daten ändern, die sie zeigt. Aber was „ihre Daten“ sind, entscheiden die beiden Bibliotheken verschieden. Beide liefern das richtige Ergebnis; der Unterschied ist, wie viel Arbeit sie dafür leisten.

React rendert die Komponente neu, deren Zustand sich geändert hat, und alle ihre Kinder, den ganzen Baum hinunter. Standardmäßig prüft es nicht, ob die Props eines Kindes dieselben sind. Hier liegt draft in App, also rendert jeder Buchstabe App, ProductList und jede Karte.

Das ist meist schnell: React vergleicht das Ergebnis mit der alten Seite und ändert im DOM nur den Unterschied. Teuer wird es, wenn der Teilbaum groß oder die Berechnung darin schwer ist.

Erst messen, dann reparieren. Ein Zähler in der Konsole wie hier ist die einfachste Messung. Die React DevTools haben einen Reiter Profiler: Du nimmst auf, tippst ins Feld und siehst, welche Komponente wie oft und in wie vielen Millisekunden gerendert hat. Du reparierst nur ein Rendern, das überflüssig und langsam ist.

Sieh es dir an

Sieh, wer aufleuchtet

const ProductList = memo(function ProductList({ products, onAdd }) {
console.log(`rendere ProductList: ${++renders}`);
return products.map((p) => <ProductCard key={p.id} product={p} onAdd={onAdd} />);
});
function App() {
const [draft, setDraft] = useState('');
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
const visible = useMemo(() => filterByName(PRODUCTS, query), [query]);
const add = useCallback(() => setCount((c) => c + 1), []);
return (
<>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<button onClick={() => setQuery(draft)}>Suchen</button>
<ProductList products={visible} onAdd={add} />
</>
);
}
Schritt für Schritt, in Worten
  1. App hält den Text des Feldes (draft) und die Suche (query). ProductList zeichnet die Karten. Die Konsole zählt, wie oft die Liste rendert.
  2. Jede Taste ändert draft. React rendert App neu und mit ihr jedes Kind: ProductList und jede Karte, obwohl die Liste dieselbe ist.
  3. memo(ProductList) vergleicht die neuen Props mit den alten. useMemo hält dasselbe Array, solange query dasselbe ist, und useCallback hält dasselbe add.
  4. Jetzt rendert eine Taste nur App und das Feld. Die Props von ProductList sind dieselben, also wird sie übersprungen, und der Zähler bleibt.
  5. „Suchen“ ändert query. Das Array ist neu, also rendert ProductList. Dieses Rendern ist nötig: Die Liste zeigt jetzt zwei Produkte.
  6. Erst messen: ein Zähler in der Konsole oder der Profiler in den DevTools. Repariere nur die Renderings, die überflüssig und langsam sind.
React kann ein Kind nur überspringen, wenn du ihm sagst, dass es die Props vergleichen soll: Das ist memo. Und der Vergleich ergibt nur Sinn, wenn die Props wirklich dieselben sind: Das Array und die Funktion müssen zwischen den Renderings erhalten bleiben.

Probier es aus

Nur „Suchen“ rendert die Liste

Sorge dafür, dass Tippen ins Feld und „Hinzufügen“ ProductList nicht noch einmal rendern. Nur „Suchen“ soll sie rendern, denn nur dann ändert sich die Liste. Die Prüfung tippt „milch“, fügt zwei Produkte hinzu, klickt auf „Suchen“ und zählt die Zeilen „rendere ProductList“ in der Konsole.

React

Probier es aus

Finde den Fehler

ProductList steckt in memo, und die Liste rendert trotzdem bei jedem Buchstaben. Welche Zeile ist schuld?

Wähle die Zeile mit dem Fehler.

Probier es aus

Ergänze

Wie bleibt die teure Liste zwischen den Renderings dieselbe?

const visible = (
  () => PRODUCTS.filter((p) => p.name.includes(query)),
  [query],
);

Die Idee

Eine lange Liste: weniger rendern

Das schnellste Element ist das, das nicht gezeichnet wird. Ein Mensch sieht 10–20 Produkte auf dem Bildschirm, nicht 2000. Also zeichnet eine lange Liste nur einen Teil:

  • „Mehr anzeigen“: die ersten 20, dann bei einem Klick 20 weitere. Gemerkt wird nur, wie viele angezeigt werden.
  • Seiten: „1, 2, 3 …“, oft mit der Nummer in der Adresse (/?page=2), damit man sie teilen kann.
  • Eine virtuelle Liste (Windowing): zeichnet nur die Zeilen, die sichtbar sind, und tauscht sie beim Scrollen aus. Sie ist schwer zu schreiben, deshalb nimmst du eine Bibliothek: TanStack Virtual oder react-window.

Die Schlüssel aus der Lektion über Listen helfen auch hier: Mit key={p.id} sind die 20 hinzugefügten Produkte neue Elemente, und die alten bleiben bestehen, ohne neu gemacht zu werden.

Die Idee

Lazy Loading

Nicht jeder Teil der Seite wird sofort gebraucht. Die Bewertungen eines Produkts, die Karte der Läden, der Profil-Editor: Sie können erst laden, wenn der Mensch sie öffnet. Dann lädt das erste Laden weniger Code herunter.

import { lazy, Suspense } from 'react';
const Reviews = lazy(() => import('./Reviews.jsx'));
<Suspense fallback={<p>Bewertungen werden geladen…</p>}>
<Reviews />
</Suspense>

import('./Reviews.jsx') ist ein Funktionsaufruf, der die Datei lädt und ein Promise zurückgibt. lazy macht daraus eine Komponente. Solange die Datei unterwegs ist, zeigt Suspense den fallback.

React

Öffne die Konsole: Die Zeile aus reviews.js kommt erst nach dem Klick. Um ehrlich zu sein: In der Sandbox sind alle Dateien schon in der Seite, deshalb führt import() die Datei hier nur später aus. In einem echten Projekt legt Vite jede solche Datei getrennt ab, und der Browser lädt sie beim ersten Klick vom Server. Dasselbe gilt für Seiten: Mit React Router kann jede Seite lazy sein.

Genauer, für Neugierige

React Compiler. Wenn das Projekt den React Compiler benutzt, fügt er dieses Merken beim Bauen des Codes von selbst hinzu, und manuelles memo, useMemo und useCallback braucht man selten. Die React-Dokumentation empfiehlt ihn für neuen Code und behält useMemo und useCallback als Mittel für genaue Kontrolle.

Zustand nach unten. Eine weitere Lösung ohne memo: Das Feld mit seinem draft wird zu einer eigenen Komponente SearchBox, die erst bei „Suchen“ nach oben meldet (onSearch(draft)). Dann ändern die Buchstaben den Zustand in SearchBox, und App rendert gar nicht. Das ist die Regel „so hoch wie nötig und nicht höher“ aus der Lektion über das Hochziehen des Zustands.

StrictMode. In der Entwicklung rendert <StrictMode> jede Komponente zweimal, um unreine Komponenten zu entdecken. Der Zähler wächst dann um zwei. Die gebaute Version tut das nicht.

useMemo ist ein Hinweis. React darf das gemerkte Ergebnis wegwerfen und neu berechnen. Der Code muss auch ohne es richtig sein.

Der Entwicklungsmodus ist langsamer. Warnungen und Prüfungen kosten Zeit. Miss die Geschwindigkeit der gebauten Version (vite build und vite preview), nicht nur in der Entwicklung.

Übung

Mehr anzeigen

Der Katalog hat 10 Produkte und zeichnet sie alle. Zeige die ersten 4, darunter „Angezeigt: 4 von 10“ und einen Button „Mehr anzeigen“, der 4 weitere hinzufügt. Sind alle angezeigt, verschwindet der Button. Die Prüfung zählt die Produkte nach jedem Klick und sieht sich den Text und den Button an.

React

Test

Teste dich

1. Die Produktseite ist langsam. Was tust du als Erstes?
2. ProductList steckt in memo. App übergibt products={PRODUCTS.filter(…)} und onAdd={() => add()}. Du tippst in das Feld von App. Rendert ProductList?
3. Der Katalog hat 5000 Produkte, und die Seite öffnet sich langsam. Was hilft am meisten?
4. Was bringt das Lazy Loading einer Komponente (lazy / defineAsyncComponent) in einem echten Projekt?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·