Verständnis und effektive Anwendung von useMemo und useCallback für Frontend-Performance
Ethan Miller
Product Engineer · Leapcell

Einführung
In der dynamischen und sich ständig weiterentwickelnden Landschaft der Frontend-Entwicklung hat sich React als Eckpfeiler etabliert. Da Anwendungen immer komplexer werden und die Erwartungen der Benutzer an schnelle, reaktionsschnelle Benutzeroberflächen steigen, wird die Leistungsoptimierung von einer Netto-Notwendigkeit zu einer Notwendigkeit. Unter den unzähligen Werkzeugen, die React zur Leistungsabstimmung bietet, tauchen useMemo und useCallback häufig in Diskussionen auf. Ihre tatsächliche Wirkung und ihre ordnungsgemäße Anwendung werden jedoch häufig missverstanden. Entwickler könnten überoptimieren oder sich umgekehrt wegen der wahrgenommenen Komplexität von ihnen zurückhalten, ohne ein klares Verständnis dafür zu haben, wann und wie diese Hooks eine Anwendung wirklich verbessern.
Dieser Artikel zielt darauf ab, useMemo und useCallback zu entmystifizieren, Sie durch ihre Funktionsweise, praktische Anwendungsfälle zu führen und Ihnen bei der Bestimmung zu helfen, wann sie wirklich Ihre Verbündeten beim Erstellen von Hochleistungs-React-Anwendungen sind.
Kernkonzepte erklärt
Bevor wir uns mit dem Optimierungspotenzial befassen, wollen wir ein grundlegendes Verständnis der Schlüsselkonzepte entwickeln, die useMemo und useCallback zugrunde liegen.
###Referentielle Gleichheit
Im Kern von useMemo und useCallback steht das Konzept der referentiellen Gleichheit. In JavaScript werden primitive Typen (Zeichenketten, Zahlen, Booleans, null, undefined, Symbole, BigInt) nach ihrem Wert verglichen. Nicht-primitive Typen (Objekte, Arrays, Funktionen) werden jedoch nach ihrer Referenz im Speicher verglichen. Zwei Objekte mit identischem Inhalt sind nicht referentiell gleich, wenn sie unterschiedliche Speicherorte belegen.
const obj1 = { a: 1 }; const obj2 = { a: 1 }; console.log(obj1 === obj2); // false const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; console.log(arr1 === arr2); // false const func1 = () => console.log('hello'); const func2 = () => console.log('hello'); console.log(func1 === func2); // false (es sei denn, func1 und func2 verweisen auf dieselbe Funktionsinstanz)
React verwendet referentielle Gleichheit, um festzustellen, ob sich Props oder State geändert haben. Wenn sich eine Prop, die ein Objekt oder eine Funktion ist, ihre Referenz ändert, wird React sie, auch wenn ihr interner Inhalt derselbe ist, als neue Prop betrachten und möglicherweise die untergeordnete Komponente neu rendern.
Memoization
Memoization ist eine Optimierungstechnik, die hauptsächlich dazu dient, Computerprogramme zu beschleunigen, indem die Ergebnisse teurer Funktionsaufrufe gespeichert und das zwischengespeicherte Ergebnis zurückgegeben wird, wenn dieselben Eingaben erneut auftreten. Im Wesentlichen handelt es sich um eine Form des Cachings für Funktionsrückgabewerte.
Der Abgleichprozess von React
React-Komponenten rendern standardmäßig neu, wenn sich ihr State oder ihre Props ändern. Obwohl der virtuelle DOM und der Abgleichalgorithmus von React hocheffizient sind, können unnötige Neu-Renderns immer noch zu Leistungsengpässen führen, insbesondere bei komplexen Komponenten oder solchen mit vielen Kindern. useMemo und useCallback tragen zur Optimierung dieses Prozesses bei, indem sie React helfen, redundante Arbeiten zu vermeiden.
Die useMemo-Hook
useMemo ist ein React-Hook, mit dem Sie das Ergebnis einer Berechnung zwischen Neu-Renderns zwischenspeichern können. Es nimmt zwei Argumente entgegen: eine "Erstellungs"-Funktion und ein Abhängigkeitsarray. Die Erstellungsfunktion wird nur neu ausgeführt, wenn sich eine der Abhängigkeiten geändert hat.
import React, { useMemo } from 'react'; function MyComponent({ list }) { // `expensiveCalculation` wird nur neu ausgeführt, wenn `list` seine Referenz ändert const expensiveResult = useMemo(() => { console.log('Durchführung komplexer Berechnung...'); return list.map(item => item * 2); // Ein Beispiel für eine teure Operation }, [list]); return ( <div> {expensiveResult.map(item => ( <span key={item}>{item} </span> ))} </div> ); }
In diesem Beispiel wird expensiveResult memoisiert. Wenn MyComponent aufgrund einer anderen Prop als list neu gerendert wird (oder sein eigener interner State geändert wird), wird die list.map-Operation nicht neu ausgeführt, und das zwischengespeicherte expensiveResult wird stattdessen verwendet. Dies spart Rechenzeit.
Die useCallback-Hook
useCallback ist ein React-Hook, mit dem Sie eine Funktionsdefinition zwischen Neu-Renderns memoisieren können. Es ist im Wesentlichen useMemo speziell für Funktionen. Es nimmt eine Funktion und ein Abhängigkeitsarray entgegen. Es gibt eine memoisierte Version des Callbacks zurück, die sich nur ändert, wenn sich eine der Abhängigkeiten geändert hat.
import React, { useState, useCallback } from 'react'; function ParentComponent() { const [count, setCount] = useState(0); // `handleClick` wird nur neu erstellt, wenn `count` seine Referenz ändert (was für eine Zahl unwahrscheinlich ist) // oder wenn sich seine Abhängigkeiten (in diesem Fall keine, aber typischerweise externer State/Props) ändern. const handleClick = useCallback(() => { setCount(c => c + 1); }, []); // Leeres Abhängigkeitsarray bedeutet, dass diese Funktion einmal erstellt und nie geändert wird return ( <div> <p>Count: {count}</p> <ChildComponent onClick={handleClick} /> </div> ); } function ChildComponent({ onClick }) { console.log('ChildComponent gerendert'); // Wird gerendert, wenn ParentComponent neu gerendert wird, sofern nicht memoisiert return <button onClick={onClick}>Increment</button>; }
In ParentComponent wird handleClick mit useCallback erstellt. Mit einem leeren Abhängigkeitsarray ([]) wird diese Funktionsinstanz nur einmal erstellt, wenn ParentComponent zuerst gerendert wird. Nachfolgende Neu-Renderns von ParentComponent führen nicht dazu, dass handleClick neu definiert wird, wodurch seine referentielle Gleichheit bewahrt bleibt.
Wann useMemo und useCallback wirklich optimieren
Der eigentliche Wert dieser Hooks ergibt sich, wenn sie unnötige Arbeit oder Neu-Renderns in bestimmten Szenarien verhindern.
Verhindern von Neu-Renderns von memoisierten untergeordneten Komponenten
Dies ist der häufigste und wirkungsvollste Anwendungsfall. Wenn Sie ein Objekt oder eine Funktion als Prop an eine untergeordnete Komponente übergeben, die mit React.memo umschlossen ist (oder eine Klassenkomponente, die PureComponent erweitert), werden useMemo und useCallback entscheidend. Ohne sie ändert sich die Referenz des Props, auch wenn sich dessen Inhalt logisch nicht geändert hat, bei jedem Neu-Rendern der Elternkomponente, was die memoisierte untergeordnete Komponente zum Neu-Rendern zwingt.
Beispiel mit useCallback:
Betrachten Sie eine Button-Komponente, die React.memo zur Optimierung verwendet:
import React, { useState, useCallback, memo } from 'react'; // Memoisierte untergeordnete Komponente const MyButton = memo(({ onClick, label }) => { console.log('Button gerendert:', label); return <button onClick={onClick}>{label}</button>; }); function Container() { const [count, setCount] = useState(0); const [toggle, setToggle] = useState(false); // Ohne useCallback wäre handleIncrement eine neue Funktion bei jedem Rendern, // was dazu führen würde, dass MyButton unnötigerweise neu gerendert wird. const handleIncrement = useCallback(() => { setCount(prevCount => prevCount + 1); }, []); // Abhängigkeiten: keine, da setCount stabil ist // Eine Funktion, die von einer State-Variablen abhängt const handleToggle = useCallback(() => { setToggle(prevToggle => !prevToggle); }, []); return ( <div> <p>Count: {count}</p> <p>Toggle: {toggle ? 'On' : 'Off'}</p> <MyButton onClick={handleIncrement} label="Increment Count" /> <MyButton onClick={handleToggle} label="Toggle" /> <button onClick={() => setCount(count + 10)}>Zwanghaftes erneutes Rendern der Elternkomponente</button> </div> ); }
Wenn auf "Zwanghaftes erneutes Rendern der Elternkomponente" geklickt wird (was count aktualisiert und Container zum Neu-Rendern veranlasst), werden Sie feststellen, dass "Button rendered: Increment Count" und "Button rendered: Toggle" nicht protokolliert werden, da handleIncrement und handleToggle die referentielle Gleichheit beibehalten und MyButton memoisiert ist. Ohne useCallback würden beide Schaltflächen neu gerendert.
Beispiel mit useMemo:
Ähnlich für Objekte, die als Props übergeben werden:
import React, { useState, useMemo, memo } from 'react'; const UserCard = memo(({ user }) => { console.log('UserCard gerendert für:', user.name); return ( <div> <h3>{user.name}</h3> <p>Alter: {user.age}</p> </div> ); }); function UserProfile() { const [age, setAge] = useState(30); const [name, setName] = useState("Alice"); const [count, setCount] = useState(0); // Unabhängiger State, um ein erneutes Rendern der Elternkomponente auszulösen // Dieses `user`-Objekt wird bei jedem Rendern neu erstellt, wenn es nicht memoisiert ist. // Mit useMemo ändert es sich nur, wenn sich `name` oder `age` ändert. const user = useMemo(() => ({ name, age }), [name, age]); return ( <div> <UserCard user={user} /> <button onClick={() => setAge(age + 1)}>Alter erhöhen</button> <button onClick={() => setCount(count + 1)}>Unabhängigen State aktualisieren ({count})</button> </div> ); }
Wenn auf "Unabhängigen State aktualisieren" geklickt wird, rendert UserProfile neu. "UserCard rendered for: Alice" wird jedoch nicht protokolliert, da die Referenz des user-Objekts dank useMemo gleich bleibt und UserCard memoisiert ist.
Vermeidung teurer Berechnungen
Wenn Sie eine Funktion oder einen Codeblock haben, der eine rechenintensive Aufgabe ausführt, und sein Ergebnis nur von bestimmten Werten abhängt, kann useMemo verhindern, dass diese Arbeit bei jedem Rendern unnötig wiederholt wird.
import React, { useState, useMemo } from 'react'; function ItemList({ items, filterText }) { const [sortOrder, setSortOrder] = useState('asc'); // Dieses Filtern und Sortieren kann teuer sein, wenn `items` groß ist. // Wir wollen es nur neu ausführen, wenn sich `items`, `filterText` oder `sortOrder` ändern. const filteredAndSortedItems = useMemo(() => { console.log('Gefilterte und sortierte Elemente neu berechnen...'); let result = items; if (filterText) { result = result.filter(item => item.name.includes(filterText)); } if (sortOrder === 'asc') { result.sort((a, b) => a.name.localeCompare(b.name)); } else { result.sort((a, b) => b.name.localeCompare(a.name)); } return result; }, [items, filterText, sortOrder]); return ( <div> <input type="text" value={filterText} /* onChange handler */ /> <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}> Sortieren: {sortOrder} </button> <ul> {filteredAndSortedItems.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> ); }
In diesem Szenario stellt useMemo sicher, dass die Filter- und Sortierlogik nur dann ausgeführt wird, wenn sich ihre relevanten Abhängigkeiten ändern, nicht bei jedem Neu-Rendern von ItemList.
Optimierung von Effekten
Bei der Arbeit mit useEffect kann das Übergeben von Funktionen oder Objekten, deren Referenz sich bei jedem Rendern ändert, dazu führen, dass der Effekt unnötigerweise erneut ausgeführt wird, was möglicherweise zu Leistungsproblemen oder Fehlern führt (z. B. wiederholte Datenabrufe). useCallback oder useMemo können diese Abhängigkeiten stabilisieren.
import React, { useState, useEffect, useCallback } from 'react'; function DataFetcher({ userId }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); // Eine Funktion zum Abrufen von Daten. Wenn sie nicht memoisiert ist, wäre sie eine neue Funktionsinstanz // bei jedem Rendern, was dazu führen würde, dass useEffect erneut ausgeführt wird, auch wenn sich userId nicht geändert hat. const fetchData = useCallback(async () => { setLoading(true); try { const response = await fetch(`/api/users/${userId}`); const result = await response.json(); setData(result); } catch (error) { console.error("Fehler beim Abrufen von Daten:", error); } finally { setLoading(false); } }, [userId]); // fetchData ändert sich nur, wenn userId sich ändert useEffect(() => { fetchData(); }, [fetchData]); // Effekt wird nur ausgeführt, wenn fetchData (und damit userId) sich ändert if (loading) return <div>Lade Daten...</div>; if (!data) return <div>Keine Daten gefunden.</div>; return <div>Benutzername: {data.name}</div>; }
Hier wird fetchData mit useCallback umschlossen. Dies stellt sicher, dass der useEffect-Hook nur dann erneut ausgeführt wird, wenn sich userId (seine Abhängigkeit) tatsächlich ändert, und verhindert so redundante API-Aufrufe.
Wann sie NICHT verwendet werden sollten (oder wann sie NICHT helfen)
Es ist ebenso wichtig zu verstehen, wann useMemo und useCallback wirkungslos oder sogar nachteilig sind.
-
Einfache Berechnungen: Bei trivialen Berechnungen oder einfachen Funktionsdefinitionen kann der Overhead von
useMemo/useCallback(Erstellung des Memoization-Caches, Vergleich von Abhängigkeiten) potenzielle Leistungsgewinne überwiegen.// Kein wirklicher Vorteil, erhöht nur den Overhead const sum = useMemo(() => a + b, [a, b]); -
Komponenten, die NICHT in
React.memoeingeschlossen sind: Wenn die untergeordnete Komponente, die die memoisierte Prop empfängt, nicht selbst memoisiert ist (mittelsReact.memooder alsPureComponent), wird sie unabhängig davon neu gerendert, ob sich ihre Props referentiell ändern. In diesem Fall hatuseMemo/useCallbackkeinen Einfluss auf die Verhinderung des Neu-Renderns der untergeordneten Komponente. -
useCallbackmit leerem Abhängigkeitsarray für State-Updates: Obwohl im Allgemeinen nützlich, kannuseCallbackmit einem leeren Abhängigkeitsarray für State-Setter irreführend sein. Die State-Setter-Funktionen von React (wiesetCount) sind stabil und werden niemals neu erstellt. Das Umschließen mituseCallbackmit einem leeren Abhängigkeitsarray ist daher überflüssig.// Überflüssig, setCount ist bereits stabil const handleSetCount = useCallback(() => setCount(0), []);Wenn die Callback-Funktion jedoch vom aktuellen State oder den Props abhängt, müssen ihre Abhängigkeiten korrekt angegeben werden.
-
Übermäßiger Gebrauch: Übermäßiger Gebrauch von
useMemounduseCallbackkann zu komplexerem Code führen, die Fehlersuche erschweren und eigenen Leistungsaufwand verursachen, wenn er nicht umsichtig angewendet wird. Messen Sie immer, bevor Sie optimieren. Der Profiler von React DevTools ist ein hervorragendes Werkzeug zur Identifizierung tatsächlicher Engpässe in der Leistung.
Fazit
useMemo und useCallback sind leistungsstarke Werkzeuge im Leistungsoptimierungs-Repertoire von React, die hauptsächlich durch Memoization und referentielle Gleichheit unnötige Berechnungen und Neu-Renderns von memoisierten Komponenten verhindern. Sie sind am effektivsten, wenn sie an memoisierte untergeordnete Komponenten übergeben werden, um Prop-Referenzen (Funktionen, Objekte) zu stabilisieren oder wenn sie die Ergebnisse wirklich teurer Berechnungen zwischenspeichern. Sie verursachen jedoch eigenen Overhead und sollten mit Bedacht angewendet werden, wobei der Schwerpunkt auf identifizierten Leistungsengpässen und nicht als universelle Lösung liegt. Das Verständnis ihrer zugrunde liegenden Prinzipien und praktischen Anwendungen befähigt Entwickler, schnellere, effizientere React-Anwendungen zu erstellen, indem sie diese Hooks strategisch dort einsetzen, wo sie echten Mehrwert bieten.