und wann schadet es der Performance?
React.memo, useMemo und useCallback sind die meistmissverstandenen Performance-Tools in React. Sie werden entweder überall eingesetzt – mit der Hoffnung, alles schneller zu machen – oder gar nicht. Beides ist falsch. Memoization hat Kosten: Vergleichsoperationen, Speicher für gecachte Werte und erhöhte Codekomplexität. Nur dort einsetzen, wo gemessene Re-Render-Probleme vorliegen.
Inhaltsverzeichnis
- 1. Wie React Re-Renders entscheidet
- 2. React.memo: Funktionsweise und Grenzen
- 3. Referential Equality: warum React.memo oft nicht hilft
- 4. useMemo: teure Berechnungen cachen
- 5. useCallback: stabile Funktionsreferenzen
- 6. Re-Renders messen mit React DevTools Profiler
- 7. Alternativen: State nach unten, Composition nach oben
- 8. Wann Memoization schadet
- 9. Memoization-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Wie React Re-Renders entscheidet
React rendert eine Komponente neu, wenn ihr State sich ändert, ihre Props sich ändern oder ihr Elternteil neu gerendert wird. Der letzte Punkt ist der wichtigste für das Verständnis von React.memo: Wenn eine Elternkomponente rendert, rendert React standardmäßig alle ihre Kinder – unabhängig davon, ob sich deren Props geändert haben. Das ist kein Bug, sondern ein bewusstes Design: Rendering ist günstig, echte DOM-Updates nicht. Der Reconciler vergleicht das neue VDOM mit dem alten und aktualisiert nur tatsächlich geänderte DOM-Knoten.
Das bedeutet: Nicht jeder Re-Render ist ein Problem. React ist dafür optimiert, viele schnelle Renders durchzuführen. Ein Re-Render wird erst dann zum Problem, wenn die Render-Funktion selbst teuer ist – etwa weil sie eine komplexe Berechnung ausführt, eine große Liste filtert oder sortiert – oder wenn er so häufig auftritt, dass der Main Thread blockiert wird. Erst wenn messbare Performance-Probleme vorliegen, ist Memoization das richtige Werkzeug. Präventive Memoization – überall React.memo und useMemo einsetzen, ohne gemessene Probleme – ist kontraproduktiv.
2. React.memo: Funktionsweise und Grenzen
React.memo ist ein Higher-Order-Component, das eine Komponente umschließt und sie nur dann neu rendert, wenn sich ihre Props geändert haben. Der Vergleich erfolgt standardmäßig durch shallow equality: Jede Prop wird mit dem strict-equality-Operator (===) verglichen. Wenn alle Props denselben Referenzwert haben wie beim letzten Render, überspringt React den Render dieser Komponente und gibt das letzte Ergebnis zurück. Wenn sich auch nur eine Prop geändert hat – oder eine neue Referenz hat, auch wenn der Wert logisch gleich ist – rendert die Komponente neu.
React.memo akzeptiert ein optionales zweites Argument: eine custom comparison-Funktion, die entscheidet, ob Props als gleich gelten sollen. Das ermöglicht Deep-Equal-Vergleiche oder selektive Prop-Vergleiche. Diese Funktion hat jedoch eine invertierte Semantik im Vergleich zu shouldComponentUpdate: Sie gibt true zurück, wenn die Komponente nicht neu gerendert werden soll (Props sind gleich), und false, wenn sie neu gerendert werden soll. Diese unintuitiv invertierte Logik ist eine häufige Fehlerquelle. Custom comparators sollten sparsam und nur mit Tests verwendet werden.
import { memo, useMemo, useCallback, useState } from 'react';
// React.memo — only re-renders when props change by reference (shallow equality)
const ExpensiveList = memo(function ExpensiveList({
items,
onItemClick,
}: {
items: string[];
onItemClick: (item: string) => void;
}) {
console.log('ExpensiveList rendered'); // should not appear on parent re-renders
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => onItemClick(item)}>{item}</li>
))}
</ul>
);
});
function Parent() {
const [count, setCount] = useState(0);
const [filter, setFilter] = useState('');
// WITHOUT useMemo: new array on every parent render → memo is bypassed
const filteredItems = useMemo(
() => allItems.filter(item => item.includes(filter)),
[filter] // stable reference when filter does not change
);
// WITHOUT useCallback: new function reference every render → memo bypassed
const handleItemClick = useCallback((item: string) => {
console.log('Clicked:', item);
}, []); // stable reference — no dependencies
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{/* memo works: items and handler have stable references when count changes */}
<ExpensiveList items={filteredItems} onItemClick={handleItemClick} />
</div>
);
}
3. Referential Equality: warum React.memo oft nicht hilft
Die häufigste Ursache dafür, dass React.memo nicht funktioniert: Props mit neuen Referenzen bei jedem Render. In JavaScript erstellt jeder Ausdruck wie [], {} oder () => {} eine neue Referenz – auch wenn Inhalt und Verhalten identisch sind. Wenn eine Komponente ihrem gememoisierten Kind ein Array übergibt, das bei jedem Render neu erstellt wird (<Child items={data.filter(...)}/>), bekommt das Kind jedes Mal eine neue Referenz. React.memo vergleicht die Referenz, nicht den Inhalt – die Komponente rendert trotz React.memo bei jedem Eltern-Render neu.
Dieses Problem betrifft drei Typen von Props: Objekte (style={{color: 'red'}} ist jedes Mal ein neues Objekt), Arrays (inline filter(), map()) und Funktionen (Inline-Arrow-Functions als Event-Handler). Die Lösung für Objekte und Arrays ist useMemo, die Lösung für Funktionen ist useCallback. Aber: Wenn React.memo ohne die Stabilisierung der Props-Referenzen eingesetzt wird, entstehen Kosten ohne Nutzen – der shallow-Vergleich wird durchgeführt und schlägt fehl, weil alle Referenzen neu sind. Schlechter als kein React.memo.
4. useMemo: teure Berechnungen cachen
useMemo cached das Ergebnis einer Berechnung zwischen Renders. Es führt die übergebene Funktion beim ersten Render aus und speichert das Ergebnis. Bei jedem folgenden Render vergleicht es die Abhängigkeiten (Dependency Array) mit dem letzten Render. Wenn alle Abhängigkeiten dieselbe Referenz haben, gibt es den gecachten Wert zurück, ohne die Funktion erneut auszuführen. Wenn sich eine Abhängigkeit geändert hat, führt es die Funktion erneut aus und speichert das neue Ergebnis.
useMemo hat zwei legitime Anwendungsfälle: erstens, teure Berechnungen zu cachen, die bei jedem Render neu ausgeführt werden würden und messbar Zeit kosten – etwa das Filtern einer Liste mit tausenden Einträgen oder die Berechnung eines Graphen-Layouts. Zweitens, stabile Referenzen für Arrays und Objekte zu erzeugen, die als Props an memoisisierte Kindkomponenten übergeben werden. Für alles andere – simple Berechnungen, String-Konkatenationen, Zugriffe auf Objekt-Properties – ist useMemo Overkill: Der Vergleich der Abhängigkeiten und die Verwaltung des Caches kosten mehr als die Berechnung selbst.
5. useCallback: stabile Funktionsreferenzen
useCallback ist im Kern identisch mit useMemo, aber speziell für Funktionen: Statt useMemo(() => () => doSomething(), [dep]) schreibt man kompakter useCallback(() => doSomething(), [dep]). Der Hauptanwendungsfall: Event-Handler-Funktionen, die als Props an memoisisierte Kindkomponenten übergeben werden. Wenn der Handler bei jedem Render neu erstellt wird, umgeht das React.memo. useCallback erzeugt eine stabile Referenz, die sich nur ändert, wenn sich die Abhängigkeiten ändern.
Ein wichtiges Muster im Zusammenhang mit useCallback: Wenn die Funktion auf State zugreift, der sich häufig ändert, muss dieser State in das Dependency Array aufgenommen werden. Das führt dazu, dass sich die Funktionsreferenz bei jeder State-Änderung ändert – womit der Vorteil von useCallback wegfällt. Die Lösung: State-Setter-Funktionen des Updater-Pattern verwenden (setState(prev => prev + 1) statt setState(count + 1)). Das Updater-Pattern braucht keine Abhängigkeit auf den aktuellen State-Wert, weil der aktuelle Wert als Argument übergeben wird.
import { memo, useMemo, useCallback, useState, useRef } from 'react';
// Measuring if memoization is actually helping
function useRenderCount(label: string) {
const count = useRef(0);
count.current += 1;
console.log(`${label} rendered ${count.current} times`);
}
const SortedTable = memo(function SortedTable({
data,
onSort,
}: {
data: { id: number; name: string; value: number }[];
onSort: (column: string) => void;
}) {
useRenderCount('SortedTable'); // verify memo is actually working
return (
<table>
<thead>
<tr>
<th onClick={() => onSort('name')}>Name</th>
<th onClick={() => onSort('value')}>Wert</th>
</tr>
</thead>
<tbody>
{data.map(row => <tr key={row.id}><td>{row.name}</td><td>{row.value}</td></tr>)}
</tbody>
</table>
);
});
function Dashboard({ rawData }: { rawData: { id: number; name: string; value: number }[] }) {
const [sortColumn, setSortColumn] = useState('name');
const [theme, setTheme] = useState('light'); // changes should NOT re-render SortedTable
// useMemo: expensive sort cached by sortColumn, not by theme
const sortedData = useMemo(
() => [...rawData].sort((a, b) => a[sortColumn] > b[sortColumn] ? 1 : -1),
[rawData, sortColumn]
);
// useCallback: stable reference — memo works even when theme changes
const handleSort = useCallback((column: string) => {
setSortColumn(column);
}, []); // no deps: uses setSortColumn which is always stable
return (
<div data-theme={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>Theme</button>
{/* SortedTable should NOT re-render when theme toggles */}
<SortedTable data={sortedData} onSort={handleSort} />
</div>
);
}
6. Re-Renders messen mit React DevTools Profiler
Bevor Memoization eingesetzt wird, muss das Problem gemessen werden. Das wichtigste Werkzeug: React DevTools Profiler. Er zeigt für jeden Render, welche Komponenten neu gerendert wurden und warum – ob wegen Props-Änderung, State-Änderung oder Eltern-Render. Der Flamegraph zeigt die Render-Dauer jeder Komponente. Rote Balken markieren die langsamsten Renders. Die "Why did this render?"-Funktion im Profiler gibt für jede Komponente den Grund an: welche Prop oder welcher State sich geändert hat.
Ein zweites nützliches Tool: Die Browser-Extension react-scan oder das Paket @welldone-software/why-did-you-render. Letzteres gibt bei jedem unnötigen Re-Render – wenn Props referenziell gleich sind, die Komponente aber trotzdem rendert – eine Console-Warnung mit der Ursache aus. Das deckt Fälle auf, wo React.memo eingesetzt wird, aber trotzdem jedes Mal rendert, weil ein Prop eine neue Referenz bekommt. Erst mit diesen Messwerkzeugen kann man fundierte Entscheidungen treffen, wo Memoization wirklich hilft.
7. Alternativen: State nach unten, Composition nach oben
Bevor React.memo eingesetzt wird, sollte geprüft werden, ob strukturelle Alternativen das Re-Render-Problem eleganter lösen. Das erste Muster: State nach unten verschieben (Colocation). Wenn ein State-Update nur einen kleinen Teil der UI betrifft, kann dieser State in die betroffene Komponente verschoben werden. Damit rendert nur diese Komponente bei State-Änderungen neu – nicht die gesamte Elternkomponente mit allen ihren Kindern. Kein React.memo nötig, kein useMemo, keine neue Komplexität.
Das zweite Muster: Composition (children als Prop). Wenn eine Komponente bei jedem Render neu rendert und dabei teure Kindkomponenten mitnimmt, kann die Kindkomponente als children-Prop übergeben werden. Da die Kindkomponente dann vom Großelternteil gerendert wird – der sich nicht ändert – wird sie nicht neu gerendert, auch wenn der Elternteil es tut. Dieses Muster ist besonders effektiv für teure Wrapper-Komponenten wie Modals, Panels und Listencontainer. Es vermeidet Memoization vollständig und macht den Code gleichzeitig lesbarer.
8. Wann Memoization schadet
Memoization schadet in mehreren konkreten Szenarien. Erstens: Wenn Props häufig und zuverlässig wechseln. Wenn eine Komponente bei jedem Render neue Props bekommt – weil der Parent immer neue Referenzen erzeugt – führt React.memo bei jedem Render einen Vergleich durch, der fehlschlägt, und rendert die Komponente trotzdem. Das ist schlechter als kein React.memo, weil der Vergleich eine zusätzliche Operation kostet ohne jemals einen Render zu verhindern.
Zweitens: Wenn useMemo für triviale Berechnungen eingesetzt wird. Eine Berechnung wie const double = useMemo(() => count * 2, [count]) ist schlechter als const double = count * 2. Die Multiplikation kostet Nanosekunden; useMemo verwaltet einen Cache, prüft Abhängigkeiten und hält einen Wert im Speicher. Drittens: Wenn React Compiler in Verwendung ist. Der React Compiler (früher React Forget), der mit React 19 eingeführt wird, analysiert Komponenten statisch und fügt Memoization automatisch und präzise dort ein, wo sie sinnvoll ist. Manuelles React.memo und useMemo werden damit weitgehend überflüssig – und können sogar mit dem Compiler interferieren.
9. Memoization-Strategien im Vergleich
Die Entscheidung, welche Memoization-Strategie eingesetzt wird, hängt vom konkreten Problem ab. Ein strukturierter Vergleich der Optionen zeigt, wann welche Maßnahme sinnvoll ist.
| Maßnahme | Löst | Kosten | Wann einsetzen |
|---|---|---|---|
| State Colocation | Unnötige Eltern-Renders | Keine | Immer zuerst prüfen |
| children Composition | Teure Kinder in Wrappern | Keine | Bei Wrapper-Komponenten |
| React.memo | Renders durch Eltern-Update | Shallow Vergleich pro Render | Teure Komponente, stabile Props |
| useMemo | Teure Berechnung / Referenz | Cache-Verwaltung, Speicher | Messbarer Rechenaufwand |
| useCallback | Neue Funktionsreferenz | Cache-Verwaltung, Dep-Vergleich | Props an memo-Komponenten |
Die Reihenfolge der Maßnahmen ist entscheidend: Erst Struktur optimieren (Colocation, Composition), dann messen (Profiler, why-did-you-render), dann gezielt memoisisieren. Memoization ohne vorherige Messung ist Spekulation. Memoization ohne Stabilisierung der Props (useMemo/useCallback) bei gleichzeitiger Verwendung von React.memo ist Mehraufwand ohne Nutzen. Der React Compiler in React 19 nimmt diesen Entscheidungsprozess langfristig ab – bis dahin gilt: erst messen, dann handeln.
Mironsoft
React Performance-Analyse, Memoization-Audit und Re-Render-Optimierung
React-App auf unnötige Re-Renders analysieren?
Wir führen eine professionelle Re-Render-Analyse eurer React-Applikation durch: Profiler-Auswertung, Identifikation teurer Komponenten und gezielte Memoization-Strategie – mit messbaren Vorher/Nachher-Ergebnissen.
Profiler-Analyse
React DevTools Profiler und why-did-you-render für alle kritischen Flows
Strukturoptimierung
State Colocation und Composition vor Memoization — Grundursachen beheben
Gezielte Memoization
React.memo, useMemo und useCallback nur dort, wo gemessener Bedarf besteht
10. Zusammenfassung
React.memo, useMemo und useCallback sind präzise Werkzeuge, keine generellen Beschleuniger. Jede Memoization hat Kosten: Vergleichsoperationen, Cache-Verwaltung und Codekomplexität. Diese Kosten lohnen sich nur, wenn sie durch vermiedene Re-Renders oder gesparte Rechenzeit überwogen werden. Vor jedem Einsatz steht die Messung mit React DevTools Profiler und why-did-you-render. Strukturelle Alternativen – State Colocation und children-Composition – lösen viele Re-Render-Probleme eleganter und ohne zusätzliche Komplexität.
Die drei häufigsten Fehler: React.memo ohne Stabilisierung der Props (useMemo/useCallback), was den Memo-Check immer fehlschlagen lässt. useMemo für triviale Berechnungen, die schneller sind als die Cache-Verwaltung selbst. useCallback mit zu vielen Abhängigkeiten, wodurch die Funktion bei jeder relevanten State-Änderung trotzdem neu erstellt wird. Der React Compiler in React 19 wird diese manuellen Entscheidungen teilweise automatisieren – bis dahin gilt: erst messen, strukturelle Alternativen prüfen, dann gezielt memoisisieren.
React.memo, useMemo, useCallback — Das Wichtigste auf einen Blick
Erst Struktur
State Colocation und children-Composition prüfen bevor memoisisiert wird. Oft lösen sie das Problem ohne jede Komplexität.
Erst messen
React DevTools Profiler und why-did-you-render einsetzen. Nur dort memoisisieren, wo messbare Re-Render-Probleme bestehen.
Props stabilisieren
React.memo ohne stabile Props-Referenzen ist wirkungslos. useMemo für Arrays/Objekte, useCallback für Funktionen als Props.
React Compiler
React 19 Compiler memoisisiert automatisch. Manuelles React.memo wird langfristig überflüssig. Heute: gezielt und gemessen einsetzen.