React-Komponenten sind Funktionen, die bei jeder Zustandsänderung erneut ausgeführt werden. Dieser Ansatz ist elegant und vorhersagbar, bringt aber eine Konsequenz mit sich: Jede Berechnung im Komponenten-Body läuft bei jedem Rendering erneut. Eine komplexe Datenfilterung, eine aufwendige Sortierung, eine mathematische Auswertung – all das wird wiederholt, selbst wenn sich die zugrundeliegenden Daten gar nicht geändert haben.
Für die meisten Berechnungen ist das kein Problem. Moderne
JavaScript-Engines sind schnell, und React ist optimiert. Aber manchmal
stößt diese Naivität an ihre Grenzen. Eine Liste mit tausend Einträgen
zu filtern und zu sortieren kann Millisekunden dauern. Bei 60 Renderings
pro Sekunde summiert sich das. Hier kommt useMemo ins Spiel
– ein Hook, der nicht neue Funktionalität ermöglicht, sondern vorhandene
Performance-Probleme löst.
Memoizierung ist eine Optimierungstechnik aus der Informatik: Das
Ergebnis einer Funktion wird gespeichert und bei erneuten Aufrufen mit
denselben Argumenten wiederverwendet, statt die Berechnung zu
wiederholen. useMemo wendet dieses Prinzip auf
React-Komponenten an.
Der Hook nimmt zwei Parameter entgegen: eine Funktion, die die Berechnung durchführt, und ein Array von Dependencies. Solange sich die Dependencies nicht ändern, gibt React das gecachte Ergebnis zurück. Ändert sich mindestens eine Dependency, wird die Funktion erneut ausgeführt und das Ergebnis aktualisiert.
const filtered = useMemo(() => {
return items.filter(item => item.active);
}, [items]);
Diese Struktur folgt dem gleichen Muster wie useEffect –
bewusst. React’s Hook-API ist konsistent gestaltet, und wer
useEffect verstanden hat, findet sich in
useMemo sofort zurecht.
Der entscheidende Unterschied: useMemo läuft während des
Renderings, nicht danach. Die Berechnung ist synchron und blockiert. Das
Ergebnis steht sofort zur Verfügung und kann direkt im JSX verwendet
werden.
Die Versuchung ist groß, useMemo überall einzusetzen.
“Mehr Performance kann nicht schaden”, könnte man denken. Die Realität
ist komplexer. useMemo hat Kosten: React muss das
Dependency Array bei jedem Rendering vergleichen und entscheiden, ob
eine Neuberechnung nötig ist. Bei trivialen Berechnungen ist dieser
Overhead oft größer als der Nutzen.
Gute Kandidaten für useMemo:
Operationen mit nachweisbarem Performance-Impact profitieren von Memoizierung. Das Filtern, Sortieren oder Transformieren großer Arrays ist der klassische Fall. Tausend Objekte durchlaufen, prüfen, umwandeln – das summiert sich.
function ProductList({ products, category }: Props) {
// Ohne useMemo: Bei jedem Render gefiltert und sortiert
// Mit useMemo: Nur wenn products oder category sich ändern
const displayProducts = useMemo(() => {
return products
.filter(p => p.category === category)
.sort((a, b) => a.price - b.price);
}, [products, category]);
return (
<ul>
{displayProducts.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
Komplexe Berechnungen, die mehrere Schritte oder rekursive Algorithmen involvieren, sind ebenfalls Kandidaten. Statistische Auswertungen, Graphentraversierungen, geometrische Berechnungen – wenn die Berechnung Zeit braucht und die Inputs sich selten ändern, lohnt sich Memoizierung.
Ein zweiter, ebenso wichtiger Anwendungsfall liegt in der Referenzstabilität. JavaScript vergleicht Objekte anhand ihrer Referenz, nicht ihres Inhalts. Ein neu erstelltes Objekt ist ein anderes Objekt, selbst wenn es identische Eigenschaften hat.
function Parent() {
// Neues Objekt bei jedem Render!
const config = { theme: 'dark', size: 'large' };
return <Child config={config} />;
}
const Child = React.memo(({ config }: Props) => {
// Rendert bei jedem Parent-Rendering neu,
// obwohl config inhaltlich gleich bleibt
return <div>{config.theme}</div>;
});
Mit useMemo können wir die Referenz stabilisieren:
function Parent() {
const config = useMemo(
() => ({ theme: 'dark', size: 'large' }),
[] // Keine Dependencies: config bleibt immer gleich
);
return <Child config={config} />;
}
Jetzt erhält Child bei jedem Rendering dasselbe Objekt.
React.memo kann seine Arbeit tun und unnötige Re-Renderings
vermeiden.
Schlechte Kandidaten für useMemo:
Triviale Berechnungen profitieren nicht. Eine Zahl verdoppeln, zwei
Strings konkatenieren, ein kleines Objekt erstellen – der Aufwand ist
minimal, und useMemo fügt nur Komplexität hinzu.
// Unnötig: Die Berechnung ist trivial
const doubled = useMemo(() => count * 2, [count]);
// Besser: Direkte Berechnung
const doubled = count * 2;
Berechnungen, deren Dependencies sich ständig ändern, gewinnen nichts durch Memoizierung. Wenn die Funktion bei jedem Rendering ohnehin neu läuft, ist der Cache nutzlos.
function Component({ timestamp }: { timestamp: number }) {
// Nutzlos: timestamp ändert sich ständig
const formatted = useMemo(() => {
return new Date(timestamp).toLocaleString();
}, [timestamp]);
}
Operationen mit Side Effects gehören nicht in useMemo.
Die Memoizierungs-Funktion sollte rein sein – gleiche Inputs müssen zu
gleichen Outputs führen, ohne externe Effekte.
// Falsch: Side Effect in useMemo
const data = useMemo(() => {
fetch('/api/data'); // ❌ Side Effect!
return someCalculation();
}, []);
// Richtig: Side Effect in useEffect
useEffect(() => {
fetch('/api/data');
}, []);
| Szenario | useMemo? | Grund |
|---|---|---|
| Liste mit 1000 Items filtern | Ja | Nachweisbarer Performance-Gewinn |
| Zahl verdoppeln | Nein | Overhead > Nutzen |
| Objekt für Child-Props erstellen | Ja | Referenzstabilität für React.memo |
| String zu UpperCase | Nein | Triviale Operation |
| Rekursiver Algorithmus | Ja | Teure Berechnung |
| Dependencies ändern sich immer | Nein | Cache wird nie genutzt |
Ein wichtiger Punkt, der oft übersehen wird: React garantiert nicht, dass memoizierte Werte tatsächlich gecached werden. Die Dokumentation ist explizit: “You may rely on useMemo as a performance optimization, not as a semantic guarantee.”
Das bedeutet: React kann entscheiden, den Cache zu verwerfen und die
Funktion erneut auszuführen – etwa wenn Speicher knapp wird oder in
bestimmten Concurrent-Mode-Szenarien. Der Code muss auch dann korrekt
funktionieren, wenn useMemo den Wert neu berechnet.
Diese Einschränkung hat praktische Konsequenzen. Wir dürfen nicht
davon ausgehen, dass eine Berechnung “nur einmal” läuft, nur weil sie in
useMemo steht. Die Funktion muss idempotent sein –
wiederholte Ausführungen mit denselben Inputs müssen dasselbe Ergebnis
liefern, ohne Nebenwirkungen.
// Gefährlich: Verlässt sich auf einmalige Ausführung
let counter = 0;
const value = useMemo(() => {
counter++; // ❌ Side Effect mit Zustandsänderung
return heavyCalculation();
}, []);
// Sicher: Reine Funktion ohne Side Effects
const value = useMemo(() => {
return heavyCalculation(); // ✓ Kann beliebig oft ausgeführt werden
}, []);
In Development Mode und Strict Mode führt React memoizierte Funktionen manchmal doppelt aus, um solche Probleme aufzudecken. Wenn die zweite Ausführung ein anderes Ergebnis liefert oder sichtbare Nebenwirkungen hat, ist das ein Warnsignal.
Das Dependency Array steuert, wann React eine Neuberechnung
durchführt. Die Regeln sind identisch zu useEffect: Jeder
Wert, der in der Funktion verwendet wird und sich zwischen Renderings
ändern kann, muss gelistet sein.
function SearchResults({ query, sortBy }: Props) {
const results = useMemo(() => {
return items
.filter(item => item.name.includes(query))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
return a.name.localeCompare(b.name);
});
}, [query, sortBy]); // Beide verwendet → beide im Array
}
Die tückischen Fälle entstehen bei Objekten und Arrays als
Dependencies. Ein inline definiertes Objekt erhält bei jedem Rendering
eine neue Referenz – useMemo sieht eine “Änderung” und
läuft erneut.
function Component({ items }: Props) {
const options = { caseSensitive: true }; // Neue Referenz!
const filtered = useMemo(() => {
return filterItems(items, options);
}, [items, options]); // options ändert sich ständig → kein Cache-Nutzen
}
Lösungen gibt es mehrere. Die einfachste: Das Objekt außerhalb der Komponente definieren, wenn es konstant ist.
const OPTIONS = { caseSensitive: true }; // Stabile Referenz
function Component({ items }: Props) {
const filtered = useMemo(() => {
return filterItems(items, OPTIONS);
}, [items]); // Nur items als Dependency
}
Wenn das Objekt von State oder Props abhängt, können wir die primitiven Werte als Dependencies verwenden:
function Component({ items, caseSensitive }: Props) {
const filtered = useMemo(() => {
return filterItems(items, { caseSensitive });
}, [items, caseSensitive]); // Primitiver Wert als Dependency
}
Oder wir verschachteln useMemo-Aufrufe:
function Component({ caseSensitive }: Props) {
const options = useMemo(
() => ({ caseSensitive }),
[caseSensitive]
);
const filtered = useMemo(() => {
return filterItems(items, options);
}, [items, options]); // options ist jetzt stabil
}
useMemo und useCallback sind eng verwandt.
Beide memoizieren, beide nutzen Dependencies, beide optimieren
Performance. Der Unterschied liegt im Rückgabewert.
useMemo führt eine Funktion aus und gibt deren Ergebnis
zurück:
const value = useMemo(() => expensiveCalculation(), [deps]);
useCallback gibt die Funktion selbst zurück, ohne sie
auszuführen:
const callback = useCallback(() => doSomething(), [deps]);
Tatsächlich ist useCallback(fn, deps) equivalent zu
useMemo(() => fn, deps). useCallback ist
syntaktischer Zucker für den speziellen Fall, dass wir eine Funktion
memoizieren wollen.
// Diese beiden sind identisch:
const handleClick = useCallback(() => {
doSomething();
}, [deps]);
const handleClick = useMemo(() => {
return () => doSomething();
}, [deps]);
Die Entscheidung, welchen Hook man verwendet, folgt der Intention: -
Memoiziere einen berechneten Wert →
useMemo - Memoiziere eine Funktion →
useCallback
function List({ items, onItemClick }: Props) {
// useMemo: Wert berechnen
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// useCallback: Funktion stabilisieren
const handleClick = useCallback(
(id: number) => onItemClick(id),
[onItemClick]
);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
Optimierung ohne Messung ist Spekulation. useMemo sollte
basierend auf nachweisbaren Performance-Problemen eingesetzt werden,
nicht aus Vorsicht oder Gewohnheit.
React Developer Tools bieten einen Profiler, der genau zeigt, welche Komponenten wie lange für Renderings benötigen. Flamegraphs visualisieren den Rendering-Baum und machen Performance-Engpässe sofort sichtbar.
function ExpensiveList({ items }: Props) {
// Vor der Optimierung: Profiler zeigt lange Render-Zeit
// Nach useMemo: Messbare Verbesserung?
const processedItems = useMemo(() => {
console.time('Processing');
const result = items.map(item => ({
...item,
processed: expensiveTransform(item)
}));
console.timeEnd('Processing');
return result;
}, [items]);
return <ul>{/* ... */}</ul>;
}
Die console.time/console.timeEnd-Methoden
sind einfache Werkzeuge für schnelle Messungen. Sie zeigen, ob die
Berechnung tatsächlich Zeit kostet und ob useMemo einen
Unterschied macht.
Wichtig: Messe in Production-Builds. Development-Builds haben zusätzliche Checks und Warnungen, die das Profil verzerren. Strict Mode verdoppelt Renderings. Was in Development langsam wirkt, kann in Production akzeptabel sein.
Der häufigste Fehler ist übereifrige Optimierung. Jeder Wert in
useMemo zu wrappen, macht den Code unlesbarer ohne
messbaren Nutzen. Die Faustregel: Erst optimieren, wenn ein Problem
messbar ist.
// Übertrieben: Triviale Berechnungen memoiziert
function Component({ firstName, lastName }: Props) {
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
); // ❌ String-Concatenation ist billig
const isActive = useMemo(
() => status === 'active',
[status]
); // ❌ Vergleich ist billig
return <div>{fullName}</div>;
}
// Angemessen: Nur teure Operationen
function Component({ firstName, lastName }: Props) {
const fullName = `${firstName} ${lastName}`; // ✓ Direkt
const isActive = status === 'active'; // ✓ Direkt
return <div>{fullName}</div>;
}
Ein subtilerer Fehler: useMemo als
State-Management-Ersatz missbrauchen. Memoizierung ist für Performance
gedacht, nicht für Zustandslogik.
// Falsch: Komplexe Logik in useMemo
const state = useMemo(() => {
if (condition1) return { type: 'A', value: 1 };
if (condition2) return { type: 'B', value: 2 };
return { type: 'C', value: 3 };
}, [condition1, condition2]);
// Richtig: useReducer für komplexe Zustandslogik
const [state, dispatch] = useReducer(reducer, initialState);
Fehlende Dependencies sind ein Klassiker. ESLint mit der
exhaustive-deps-Regel ist unverzichtbar – sie warnt vor
vergessenen Dependencies.
function Component({ items, query }: Props) {
const filtered = useMemo(() => {
return items.filter(item => item.name.includes(query));
}, [items]); // ❌ query fehlt im Array!
// ESLint warnt:
// React Hook useMemo has a missing dependency: 'query'
}
Ein bewährtes Pattern ist die Kombination von useMemo
mit Context, um teure Berechnungen aus dem Rendering-Pfad zu holen.
function DataProvider({ children }: Props) {
const [rawData, setRawData] = useState([]);
// Teure Verarbeitung, aber memoiziert
const processedData = useMemo(() => {
return rawData.map(item => ({
...item,
computed: expensiveComputation(item)
}));
}, [rawData]);
return (
<DataContext.Provider value={processedData}>
{children}
</DataContext.Provider>
);
}
Consumer des Context erhalten bereits verarbeitete Daten. Die teure Berechnung läuft nur einmal, im Provider, nicht in jedem Consumer.
Für Selektoren – Funktionen, die spezifische Daten aus einem größeren
State extrahieren – ist useMemo ideal:
function useFilteredProducts(category: string) {
const allProducts = useContext(ProductContext);
return useMemo(() => {
return allProducts.filter(p => p.category === category);
}, [allProducts, category]);
}
// In der Komponente:
function CategoryView({ category }: Props) {
const products = useFilteredProducts(category);
// Rendert nur neu, wenn sich filtered products ändern
return <ProductList products={products} />;
}
Bei Listen mit dynamischen Items kann useMemo helfen,
die Berechnung von Item-Props zu optimieren:
function List({ items }: Props) {
const itemsWithHandlers = useMemo(() => {
return items.map(item => ({
...item,
onClick: () => handleClick(item.id),
onHover: () => handleHover(item.id)
}));
}, [items]); // Handlers nur neu erstellen, wenn items sich ändern
return (
<ul>
{itemsWithHandlers.map(item => (
<ListItem key={item.id} {...item} />
))}
</ul>
);
}
useMemo ist kein Allheilmittel, sondern ein gezieltes
Werkzeug für nachweisbare Performance-Probleme. Richtig eingesetzt, kann
es Anwendungen signifikant beschleunigen. Falsch eingesetzt, fügt es nur
Komplexität hinzu. Die Kunst liegt darin, zu erkennen, wo die Grenze
verläuft – und das gelingt nur durch Messung, Verständnis des
Rendering-Verhaltens und Erfahrung mit real-world Anwendungen.