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.
Konsole
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
- 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.
- Jede Taste ändert draft. React rendert App neu und mit ihr jedes Kind: ProductList und jede Karte, obwohl die Liste dieselbe ist.
- memo(ProductList) vergleicht die neuen Props mit den alten. useMemo hält dasselbe Array, solange query dasselbe ist, und useCallback hält dasselbe add.
- Jetzt rendert eine Taste nur App und das Feld. Die Props von ProductList sind dieselben, also wird sie übersprungen, und der Zähler bleibt.
- „Suchen“ ändert query. Das Array ist neu, also rendert ProductList. Dieses Rendern ist nötig: Die Liste zeigt jetzt zwei Produkte.
- Erst messen: ein Zähler in der Konsole oder der Profiler in den DevTools. Repariere nur die Renderings, die überflüssig und langsam sind.
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.
Konsole
Lösung
App.jsximport { useCallback, useMemo, useState } from 'react';
import ProductList from './ProductList.jsx';
import { PRODUCTS } from './products.js';
export default function App() {
const [draft, setDraft] = useState('');
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
const visible = useMemo(() => PRODUCTS.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())), [query]);
const add = useCallback(() => setCount((c) => c + 1), []);
function search(e) {
e.preventDefault();
setQuery(draft);
}
return (
<>
<header>
<form role="search" onSubmit={search}>
<input type="search" aria-label="Suche" value={draft} onChange={(e) => setDraft(e.target.value)} />
<button>Suchen</button>
</form>
<p>Im Warenkorb: {count}</p>
</header>
<main>
<ProductList products={visible} onAdd={add} />
</main>
</>
);
}
ProductList.jsximport { memo } from 'react';
import { money } from './products.js';
let renders = 0;
function ProductList({ products, onAdd }) {
console.log(`rendere ProductList: ${++renders}`);
return (
<ul>
{products.map((p) => (
<li key={p.id}>
<span>{p.name}, {money(p.price)}</span>
<button type="button" aria-label={`Hinzufügen ${p.name}`} onClick={() => onAdd(p)}>
Hinzufügen
</button>
</li>
))}
</ul>
);
}
export default memo(ProductList);
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 2, name: 'Vollkornbrot', price: 1.9 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 6, name: 'Fettarme Milch', price: 0.9 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 8, name: 'Kaschkawal', price: 9.5 },
{ id: 9, name: 'Butter', price: 3.4 },
{ id: 10, name: 'Eier (10 Stk.)', price: 2.8 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; align-items: center; gap: 8px; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button, input { font: inherit; }
Die drei Teile gehören zusammen:
- In
ProductList.jsx:export default memo(ProductList);. Jetzt vergleicht React die Props vor dem Rendern. - In
App.jsx:const visible = useMemo(() => PRODUCTS.filter(…), [query]);. Das Array bleibt dasselbe, solangequerydasselbe ist. const add = useCallback(() => setCount((c) => c + 1), []);. Die Funktion ist bei jedem Rendern dieselbe. MitsetCount((c) => c + 1)liest siecountnicht, deshalb ist die leere Liste[]richtig.useCallback(() => setCount(count + 1), [])merkt sich das erstecount, und der Warenkorb bleibt bei 1 stehen: Die Prüfung erkennt das.
memo allein reicht nicht: Der Filter und die Funktion sind bei jedem Rendern von App neu, und memo sieht neue Props.
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.
add ist bei jedem Rendern von App ein neuer Pfeil. memo sieht ein neues onAdd und rendert ProductList. Es muss const add = useCallback(() => setCount((c) => c + 1), []); heißen.Probier es aus
Ergänze
Wie bleibt die teure Liste zwischen den Renderings dieselbe?
const visible = (
() => PRODUCTS.filter((p) => p.name.includes(query)),
[query],
);useMemo: führt die Funktion beim ersten Rendern aus und danach nur, wenn sich eine Abhängigkeit in [query] ändert. Sonst gibt es dasselbe Array zurück.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.
Konsole
Ö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.
Konsole
Lösung
App.jsximport { useState } from 'react';
import { PRODUCTS, money } from './products.js';
const PAGE = 4;
export default function App() {
const [limit, setLimit] = useState(PAGE);
const shown = PRODUCTS.slice(0, limit);
return (
<main>
<ul>
{shown.map((p) => (
<li key={p.id}>
<span>{p.name}, {money(p.price)}</span>
</li>
))}
</ul>
<p>{`Angezeigt: ${shown.length} von ${PRODUCTS.length}`}</p>
{limit < PRODUCTS.length && (
<button type="button" onClick={() => setLimit(limit + PAGE)}>Mehr anzeigen</button>
)}
</main>
);
}
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 2, name: 'Vollkornbrot', price: 1.9 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 6, name: 'Fettarme Milch', price: 0.9 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 8, name: 'Kaschkawal', price: 9.5 },
{ id: 9, name: 'Butter', price: 3.4 },
{ id: 10, name: 'Eier (10 Stk.)', price: 2.8 },
{ id: 11, name: 'Äpfel (1 kg)', price: 2.2 },
{ id: 12, name: 'Bananen (1 kg)', price: 2.6 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; align-items: center; gap: 8px; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button, input { font: inherit; }
Du merkst dir nur, wie viele angezeigt werden: const [limit, setLimit] = useState(4);. Die angezeigten werden berechnet: PRODUCTS.slice(0, limit). Der Button ist {limit < PRODUCTS.length && <button …>}, und der Klick ist setLimit(limit + 4). Eine normale Variable let limit = 4 ändert sich beim Klick, aber React rendert nicht neu: Die Prüfung erkennt das.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Effekte · Routing
- Aufgaben zum Thema: Eine ganze App