Menu

React useMemo Hook: Teure Berechnungen cachen

useMemo cacht das Ergebnis einer Berechnung zwischen Renders und berechnet es nur neu, wenn sich seine Abhängigkeiten ändern. Lerne die Syntax, wie du ein Objekt für ein Kind mit memo stabil hältst und wann sich useMemo nicht lohnt.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

useMemo cacht das Ergebnis einer Berechnung zwischen Renders. React führt deine Funktion beim ersten Render aus, behält das Ergebnis und gibt bei späteren Renders genau dieses Ergebnis zurück, bis sich einer der Werte im Abhängigkeitsarray ändert.

Öffne die Konsole und schalte das Theme ein paar Mal um: Nichts wird geloggt, und das Umschalten fühlt sich sofort an. Wechsle die Tabs, und filtering for active erscheint, mit einer kurzen Pause, weil tab eine Abhängigkeit ist. Ersetze jetzt die Zeile mit useMemo durch const visible = filterTodos(todos, tab); und schalte das Theme erneut um: Jeder Klick loggt und hängt 100 ms.

Die Syntax

const cachedValue = useMemo(calculateValue, dependencies);

const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  • calculateValue ist eine Funktion ohne Argumente, die den Wert zurückgibt, den du cachen willst. React ruft sie beim Rendern auf.
  • dependencies ist die Liste aller Werte aus der Komponente, die die Funktion liest: Props, State und im Komponentenkörper deklarierte Variablen.
  • Bei jedem Render vergleicht React jede Abhängigkeit mit ihrem Wert aus dem vorherigen Render per Object.is. Stimmen alle überein, gibt es den gecachten Wert zurück, ohne deine Funktion aufzurufen. Weicht einer ab, ruft es die Funktion auf und cacht das neue Ergebnis.

useMemo ist ein Hook, also folgt es den Regeln der Hooks: Ruf es auf der obersten Ebene der Komponente auf, nicht in einer Schleife oder einer Bedingung.

Abhängigkeiten müssen vollständig sein

Lass einen Wert aus dem Array weg, und der Cache liefert weiter ein Ergebnis, das aus seinem alten Wert berechnet wurde. Hier liest die Summe qty, listet aber nur price auf:

Klicke auf Quantity: Es geht auf 2, aber die Summe bleibt bei 10, und nichts wird geloggt. Klicke auf Price, und die Summe springt auf 22, weil die Berechnung erst dann erneut läuft und jetzt das aktuelle qty liest. Füge qty zum Array hinzu, und beide Buttons aktualisieren die Summe. Die Lint-Regel react-hooks/exhaustive-deps findet diesen Fehler in einem echten Projekt.

Der umgekehrte Fehler ist eine Abhängigkeit, die bei jedem Render neu ist, etwa ein im Komponentenkörper erzeugtes Objekt. Es stimmt nie mit seinem vorherigen Wert überein, also wird der Cache nie genutzt:

const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time

// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);

Ein Objekt für ein Kind mit memo stabil halten

Der zweite Grund, zu useMemo zu greifen, hat nichts mit langsamen Berechnungen zu tun. Ein in memo gepacktes Kind überspringt seinen Render nur, wenn jede Prop dieselbe wie beim letzten Mal ist. Ein Objektliteral im Komponentenkörper ist bei jedem Render ein neues Objekt, also hebelt es memo aus, auch wenn sich sein Inhalt nicht geändert hat. useMemo behält dasselbe Objekt, bis sich seine Eingaben ändern.

Tippe ins Notizfeld, und Chart bleibt still, während Switch unit einen Render loggt. Ersetze den Aufruf von useMemo durch ein einfaches const options = { unit, max: unit === 'km' ? 100 : 62 };, und jeder Tastendruck in der Notiz loggt render Chart.

Dasselbe gilt für einen Effekt, der von einem Objekt abhängt. Wenn ein Effekt options in seinen Abhängigkeiten führt und options bei jedem Render neu gebaut wird, läuft der Effekt nach jedem Render. useMemo behebt das, aber die einfachere Lösung ist oft, das Objekt im Effekt zu erzeugen und von den primitiven Werten abzuhängen:

useEffect(() => {
    const options = { serverUrl, roomId };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
}, [serverUrl, roomId]);

Wann du useMemo nicht nutzen solltest

Die meisten Berechnungen in einer Komponente sind billig: Zahlen addieren, zwei Strings verbinden, ein paar Dutzend Einträge filtern. Sie zu cachen spart fast nichts: React speichert trotzdem den Wert und vergleicht bei jedem Render die Abhängigkeiten, und der Code wird schwerer lesbar.

// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;

Um herauszufinden, ob etwas langsam ist, miss die Zeit. Umschließe die Berechnung mit console.time('filter') und console.timeEnd('filter'), führe die Interaktion aus und schau auf die Zahl. Als grobe Orientierung ist Arbeit, die auf deinem Rechner eine Millisekunde oder mehr dauert, ein Kandidat, wobei die Geräte deiner Nutzer langsamer sein können. Bevor du zu useMemo greifst, prüfe auch, ob die Arbeit überhaupt ins Rendern gehört: Daten, die sich nie ändern, lassen sich einmal außerhalb der Komponente berechnen, und State, den nur ein kleiner Teil der Seite nutzt, kann in eine kleinere Komponente wandern (siehe Rendern).

useMemo vs. useEffect für abgeleitete Werte

Ein häufiges Muster ist, einen Wert in einem Effekt zu berechnen und im State zu speichern:

// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
    setVisible(filterTodos(todos, tab));
}, [todos, tab]);

// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);

Die Version mit Effekt rendert einmal mit dem alten visible, führt dann den Effekt aus, setzt State und rendert erneut. Das ist ein zusätzlicher Render, und im ersten passt die Liste nicht zum Tab. Beim Rendern mit useMemo zu berechnen (oder ganz ohne Hook, wenn die Arbeit billig ist) liefert den richtigen Wert im selben Render. Effekte sind dafür da, sich mit Dingen außerhalb von React zu synchronisieren, nicht dafür, einen State-Wert aus einem anderen abzuleiten.

Ein Performance-Hinweis, keine Garantie

useMemo sagt React, dass es ein Ergebnis wiederverwenden darf. Es verspricht nicht, dass es das tut. Im normalen Betrieb behält React den gecachten Wert, kann ihn aber verwerfen, zum Beispiel wenn du die Datei in der Entwicklung bearbeitest oder wenn eine Komponente beim ersten Mounten suspendiert. Schreib den Code so, dass er auch dann korrekt ist, wenn die Berechnung erneut läuft: Die Funktion muss pur sein, ohne Nebeneffekte wie Anfragen, Abonnements oder Änderungen an Variablen außerhalb. Nebeneffekte gehören in einen Event-Handler oder einen Effekt.

In der Entwicklung ruft React unter StrictMode deine Berechnungsfunktion zweimal auf, damit dir unpurer Code auffällt. Die Vorschau hier läuft wie ein Produktions-Build, also siehst du jedes Log filtering for einmal.

useMemo und der React Compiler

React Compiler 1.0 ist ein Build-Tool, das deine Komponenten analysiert und automatisch Memoisierung hinzufügt, indem es Berechnungen und JSX an den Stellen cacht, an denen useMemo, useCallback und memo helfen würden. In einem Projekt, das ihn nutzt, schreibst du weniger davon von Hand. Manuelles useMemo funktioniert weiterhin neben dem Compiler, ist weiterhin das Werkzeug, wenn der Compiler nicht eingerichtet ist, und begegnet dir in den meisten bestehenden React-Codebasen.

Häufig gestellte Fragen

Was macht useMemo in React?

Es ruft deine Berechnungsfunktion beim ersten Render auf, speichert das Ergebnis und gibt bei späteren Renders das gespeicherte Ergebnis zurück, solange jeder Wert im Abhängigkeitsarray derselbe wie beim letzten Mal ist (verglichen mit Object.is).

Wann sollte ich useMemo nutzen?

Wenn eine Berechnung spürbar langsam ist und sich ihre Eingaben selten ändern, oder wenn ein Objekt oder Array zwischen Renders dieselbe Referenz behalten muss, weil ein Kind mit memo oder ein Effekt davon abhängt. Bei billiger Arbeit lass es weg.

Was ist der Unterschied zwischen useMemo und useCallback?

useMemo cacht den Wert, den deine Funktion zurückgibt. useCallback cacht die Funktion selbst. useCallback(fn, deps) ist dasselbe wie useMemo(() => fn, deps).

Garantiert useMemo, dass der Wert nie neu berechnet wird?

Nein. Es ist ein Hinweis für die Performance. React kann den gecachten Wert in manchen Fällen verwerfen, zum Beispiel in der Entwicklung, wenn du die Datei bearbeitest, also muss dein Code auch dann korrekt funktionieren, wenn die Berechnung erneut läuft.

Brauche ich mit dem React Compiler noch useMemo?

Der React Compiler kann Memoisierung automatisch hinzufügen, wenn dein Build ihn nutzt. Ohne Compiler oder in Fällen, die er auslässt, funktioniert manuelles useMemo weiterhin genauso und ist in bestehendem Code weiterhin üblich.

Illustration der Programmiersprachen bei Coddy

Lerne mit Coddy zu programmieren

LOS GEHT'S