19 useMemo – Rechenaufwand sparen durch gezieltes Caching

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.

19.1 Das Konzept der Memoizierung

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.

19.2 Wann useMemo sinnvoll ist – und wann nicht

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

19.3 Die Grenzen der Garantie

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.

19.4 Dependencies: Das Herz der Memoizierung

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
}

19.5 Der Unterschied zu useCallback

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 WertuseMemo - Memoiziere eine FunktionuseCallback

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>
  );
}

19.6 Performance messen, nicht raten

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.

19.7 Typische Fehler und ihre Vermeidung

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'
}

19.8 Praktische Patterns aus dem Entwickleralltag

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.