Optimierung der React-Performance mit Memoization-Techniken
Min-jun Kim
Dev Intern · Leapcell

Einleitung
In der dynamischen Welt der Front-End-Entwicklung ist die Bereitstellung einer reibungslosen und reaktionsschnellen Benutzererfahrung von größter Bedeutung. React, eine deklarative und komponentenbasierte Bibliothek, vereinfacht den Aufbau komplexer UIs. Mit zunehmender Größe und Komplexität von Anwendungen können unnötige Komponenten-Neubewertungen jedoch zu einem erheblichen Performance-Engpass werden. Diese Neubewertungen können zu trägen Oberflächen, erhöhtem CPU-Verbrauch und einer beeinträchtigten Benutzererfahrung führen. Dieser Artikel befasst sich mit der entscheidenden Rolle von Memoization-Techniken – insbesondere React.memo
, useCallback
und useMemo
– bei der Verhinderung dieser überflüssigen Neubewertungen, wodurch Ihre React-Anwendungen optimiert und eine schnellere, effizientere Benutzeroberfläche gewährleistet wird.
Kernkonzepte verstehen
Bevor wir uns mit den Mechanismen der Memoization befassen, wollen wir einige grundlegende Konzepte, die diesen Optimierungstechniken zugrunde liegen, klar verstehen.
Neubewertung (Re-rendering): In React „bewertet“ sich eine Komponente neu, wenn sich ihr Zustand oder ihre Props ändern. Wenn sich eine Elternkomponente neu bewertet, bewerten sich standardmäßig auch alle ihre Kindkomponenten neu, unabhängig davon, ob sich ihre Props tatsächlich geändert haben. Diese Kaskade von Neubewertungen ist oft die Ursache für Performance-Probleme.
Memoization: Im Kern ist Memoization eine Optimierungstechnik, die hauptsächlich dazu dient, Computerprogramme zu beschleunigen, indem die Ergebnisse kostspieliger Funktionsaufrufe gespeichert und das zwischengespeicherte Ergebnis zurückgegeben wird, wenn dieselben Eingaben erneut auftreten. In React wenden wir dieses Konzept auf Komponenten und Funktionen an, um kostspielige Operationen nicht erneut ausführen zu müssen.
Referentielle Gleichheit (Referential Equality): Dieses Konzept ist entscheidend für das Verständnis, wie Memoization in React funktioniert. In JavaScript werden Objekte und Arrays nach Referenz und nicht nach Wert verglichen. Das bedeutet, dass zwei Objekte mit identischen Eigenschaften, aber unterschiedlichen Speicheradressen als ungleich gelten. Zum Beispiel ergibt {} === {}
false
. Viele gängige Performance-Fallstricke entstehen dadurch, dass bei jeder Neubewertung unbeabsichtigt neue Objekt- oder Array-Referenzen erstellt werden, auch wenn sich ihr Inhalt nicht geändert hat.
Verhinderung unnötiger Neubewertungen
React bietet drei leistungsstarke Werkzeuge für die Memoization: React.memo
für Komponenten, useCallback
für Funktionen und useMemo
für Werte. Lassen Sie uns jeden davon mit praktischen Beispielen im Detail untersuchen.
React.memo
für Komponentenoptimierung
React.memo
ist eine Higher-Order Component (HOC), die eine funktionale Komponente umschließt. Sie „memoisiert“ die Renderausgabe der Komponente und rendert die Komponente nur dann neu, wenn sich ihre Props seit der letzten Rendereinheit flach geändert haben. Dies ist besonders nützlich für präsentationsorientierte Komponenten, die häufig dieselben Props über mehrere Rendereinheiten hinweg erhalten.
Betrachten Sie eine einfache ChildComponent
, die eine Nachricht anzeigt:
import React from 'react'; const ChildComponent = ({ message }) => { console.log('ChildComponent re-rendered'); return <p>{message}</p>; }; export default ChildComponent;
Verwenden wir sie nun in einer ParentComponent
:
import React, { useState } from 'react'; import ChildComponent from './ChildComponent'; const ParentComponent = () => { const [count, setCount] = useState(0); const fixedMessage = "Hello from child!"; return ( <div> <h1>Parent Count: {count}</h1> <button onClick={() => setCount(prev => prev + 1)}>Increment Count</button> <ChildComponent message={fixedMessage} /> </div> ); }; export default ParentComponent;
Wenn Sie auf die Schaltfläche „Increment Count“ klicken, wird die ParentComponent
neu bewertet. Infolgedessen wird auch ChildComponent
neu bewertet, und Sie sehen „ChildComponent re-rendered“ in der Konsole, auch wenn sich ihre message
-Prop nicht geändert hat.
Um diese unnötige Neubewertung zu verhindern, können wir ChildComponent
mit React.memo
umschließen:
import React from 'react'; const ChildComponent = ({ message }) => { console.log('Memoized ChildComponent re-rendered'); return <p>{message}</p>; }; export default React.memo(ChildComponent); // <--- React.memo hier anwenden
Nun, wenn sich ParentComponent
aufgrund einer Änderung von count
neu bewertet, wird ChildComponent
nur dann neu bewertet, wenn sich ihre message
-Prop ändert. Da fixedMessage
konstant bleibt, wird der Konsolen-Log nicht mehr angezeigt, wenn sich count
ändert.
Wann React.memo
verwendet werden sollte:
- Präsentationskomponenten: Komponenten, die hauptsächlich Daten anzeigen und nur minimale interne Zustände haben.
- Komponenten mit aufwendiger Rendereinheit: Wenn die Rendermechanik einer Komponente rechenintensiv ist.
- Komponenten, die oft unveränderte Props erhalten: Wenn Kindkomponenten Props erhalten, die sich selten ändern, auch wenn sich ihre Eltern häufig neu bewerten.
Vorbehalt: React.memo
führt einen flachen Vergleich der Props durch. Wenn eine Prop ein Objekt oder Array ist und sich ihr Inhalt ändert, aber ihre Referenz gleich bleibt, verhindert React.memo
keine Neubewertung. Hier kommen useCallback
und useMemo
ins Spiel.
useCallback
zum Memoizen von Funktionen
Beim Übergeben von Callback-Funktionen als Props an Kindkomponenten, insbesondere an memoizierte (wie die mit React.memo
umwickelten), ist es wichtig sicherzustellen, dass sich die Referenz der Funktion bei jeder Neubewertung der Eltern nicht ändert. Wenn dies geschieht, wird die Kindkomponente immer noch neu bewertet, wodurch die Vorteile von React.memo
zunichte gemacht werden. useCallback
hilft dabei, die Funktion selbst zu memoizen.
Lassen Sie uns unser Beispiel ändern, um eine Funktion an ChildComponent
zu übergeben:
import React, { useState } from 'react'; const MemoizedChildComponent = React.memo(({ onClick }) => { console.log('MemoizedChildComponent re-rendered'); return <button onClick={onClick}>Click Child</button>; }); const ParentComponent = () => { const [count, setCount] = useState(0); const handleClick = () => { console.log('Child button clicked!'); }; return ( <div> <h1>Parent Count: {count}</h1> <button onClick={() => setCount(prev => prev + 1)}>Increment Count</button> <MemoizedChildComponent onClick={handleClick} /> </div> ); }; export default ParentComponent;
Obwohl MemoizedChildComponent
memoisiert ist, wird bei jeder Neubewertung von ParentComponent
eine neue handleClick
-Funktionsreferenz erstellt. Da sich die Referenz der onClick
-Prop ändert, wird MemoizedChildComponent
trotzdem neu bewertet.
Um dies zu beheben, verwenden wir useCallback
:
import React, { useState, useCallback } from 'react'; const MemoizedChildComponent = React.memo(({ onClick }) => { console.log('MemoizedChildComponent re-rendered'); return <button onClick={onClick}>Click Child</button>; }); const ParentComponent = () => { const [count, setCount] = useState(0); const handleClick = useCallback(() => { // <--- Memoize the function console.log('Child button clicked!'); }, []); // Leeres Abhängigkeitsarray bedeutet, dass es nur einmal erstellt wird return ( <div> <h1>Parent Count: {count}</h1> <button onClick={() => setCount(prev => prev + 1)}>Increment Count</button> <MemoizedChildComponent onClick={handleClick} /> </div> ); }; export default ParentComponent;
Jetzt ist handleClick
memoisiert. Solange sich seine Abhängigkeiten (in diesem Fall keine, wegen []
) nicht ändern, gibt useCallback
über Neubewertungen hinweg dieselbe Funktionsinstanz zurück. Dies stellt sicher, dass MemoizedChildComponent
nur dann neu bewertet wird, wenn sich ihre onClick
-Prop (die Funktionsreferenz) tatsächlich ändert.
Abhängigkeitsarray (Dependency Array): Das zweite Argument von useCallback
ist ein Abhängigkeitsarray. Wenn sich ein Wert in diesem Array ändert, gibt useCallback
eine neue Funktionsinstanz zurück. Es ist wichtig, alle Werte aus dem Gültigkeitsbereich der Komponente einzuschließen, die innerhalb der memoizierten Funktion verwendet werden. Das Vergessen von Abhängigkeiten kann zu veralteten Closures (Funktionen, die veraltete Werte verwenden) führen.
useMemo
zum Memoizen von Werten
useMemo
ähnelt useCallback
, aber anstatt eine Funktion zu memoizen, memoisiert es einen berechneten Wert. Dies ist nützlich für aufwendige Berechnungen oder zum Erstellen von Objekt-/Array-Literalen, die beim Übergeben als Props an memoizierte Kindkomponenten die referentielle Gleichheit beibehalten müssen.
Stellen Sie sich ein Szenario vor, in dem wir eine aufwendige Berechnung haben:
import React, { useState, useMemo } from 'react'; const MemoizedChildComponent = React.memo(({ data }) => { console.log('MemoizedChildComponent re-rendered'); return ( <ul> {data.map(item => <li key={item}>{item}</li>)} </ul> ); }); const ParentComponent = () => { const [count, setCount] = useState(0); const expensiveCalculation = (num) => { console.log('Performing expensive calculation...'); let result = 0; for (let i = 0; i < 100000000; i++) { result += i; } return result + num; }; const calculatedValue = expensiveCalculation(count); // Diese Berechnung läuft bei jeder Rendereinheit return ( <div> <h1>Parent Count: {count}</h1> <button onClick={() => setCount(prev => prev + 1)}>Increment Count</button> <p>Expensive Result: {calculatedValue}</p> <MemoizedChildComponent data={['item1', 'item2']} /> </div> ); }; export default ParentComponent;
Hier läuft expensiveCalculation
jedes Mal, wenn sich ParentComponent
neu bewertet, auch wenn sich count
(die Eingabe für die Berechnung) im Vergleich zur letzten Ausführung von expensiveCalculation
nicht geändert hat.
Wir können dies mit useMemo
optimieren:
import React, { useState, useMemo } from 'react'; const MemoizedChildComponent = React.memo(({ data }) => { console.log('MemoizedChildComponent re-rendered'); return ( <ul> {data.map(item => <li key={item}>{item}</li>)} </ul> ); }); const ParentComponent = () => { const [count, setCount] = useState(0); const expensiveCalculation = (num) => { console.log('Performing expensive calculation...'); let result = 0; for (let i = 0; i < 100000000; i++) { result += i; } return result + num; }; const calculatedValue = useMemo(() => { // <--- Memoize the value return expensiveCalculation(count); }, [count]); // Nur neu berechnen, wenn sich 'count' ändert // Beispiel für das Memoizen eines Objektliterals zur Beibehaltung der referentiellen Gleichheit const listData = useMemo(() => ['item1', 'item2', `Count: ${count}`], [count]); return ( <div> <h1>Parent Count: {count}</h1> <button onClick={() => setCount(prev => prev + 1)}>Increment Count</button> <p>Expensive Result: {calculatedValue}</p> <MemoizedChildComponent data={listData} /> </div> ); }; export default ParentComponent;
Nun wird expensiveCalculation
innerhalb von useMemo
nur ausgeführt, wenn sich count
ändert. Andernfalls gibt useMemo
den zuvor berechneten Wert zurück. Ebenso erhält listData
nur dann eine neue Referenz, wenn sich count
ändert, was verhindert, dass MemoizedChildComponent
neu bewertet wird, es sei denn, es ist unbedingt erforderlich.
Wann useMemo
verwendet werden sollte:
- Aufwendige Berechnungen: Wenn ein Wert aus Props oder Zustand abgeleitet wird und die Berechnung rechenintensiv ist.
- Beibehaltung der referentiellen Gleichheit: Beim Übergeben von Objekten oder Arrays als Props an memoizierte Kindkomponenten, um unnötige Neubewertungen der Kindkomponente zu verhindern.
Wichtiger Hinweis: useMemo
und useCallback
sollten nicht wahllos verwendet werden. React Hooks haben einigen Overhead. Verwenden Sie sie hauptsächlich für Performance-Optimierungen, bei denen Sie einen Engpass identifiziert haben, oder zur Beibehaltung der referentiellen Gleichheit für memoizierte Kindkomponenten. Übermäßiger Gebrauch kann manchmal zu mehr Komplexität führen und die Performance sogar leicht reduzieren, da der Overhead von Memoization-Prüfungen anfällt.
Fazit
React.memo
, useCallback
und useMemo
sind unschätzbare Werkzeuge im Werkzeugkasten eines React-Entwicklers zum Erstellen hochperformanter Anwendungen. Durch die intelligente Anwendung dieser Memoization-Techniken können Sie unnötige Komponenten-Neubewertungen effektiv vermeiden, den Rechenaufwand erheblich reduzieren und ein reibungsloseres, reaktionsschnelleres Benutzererlebnis liefern. Die strategische Nutzung dieser Hooks ermöglicht es Ihnen, die erneute Ausführung kostspieliger Operationen zu verhindern und somit die Effizienz und Reaktionsfähigkeit Ihrer React-Anwendung zu optimieren.