alle Artikel
11 Minuten

React.memo() verstehen und anwenden

Frank Lechner ·
„React.memo() kann tricky sein - lerne wie du Fallstricke vermeidest und welche Alternativen es gibt”
React.memo() verstehen und anwenden
Dieser Artikel wurde zuletzt am bearbeitet!

React.memo() ist eine nützliche Funktion deren sinnvolle Verwendung vielen unklar ist. Die Meinungen für die sinnvolle Verwendung von memo() reichen von nie verwenden bis zu immer verwenden, manche nennen es gut, andere böse. Wenn wundert es da, das es bei diesem Thema viel Verwirrung gibt! Mit diesem Artikel möchte ich den Nebel um memo() lichten und die Frage ob memo() nun gut oder böse ist, versachlichen. Du lernst wie du memo() richtig verwendest und wann du es verwendest! Dazu verwende ich viele Code-Beispiele und viel Live Code auf CodeSandbox. Durch den Live Code kannst du die Beispiele selbst ausprobieren und so besser nachvollziehen.

1. Das Grundproblem: Referenzielle Gleichheit

Bei JavaScript muss man zwischen Wertgleichheit und Referenzgleichheit unterscheiden. Bei primitiven Werten wie Strings, Zahlen oder Boolean ist das relativ einfach:

const a = 42;
const b = 42;

console.log(a === b); // true

Bei Objekten, Arrays und Funktionen sieht es anders aus:

const a = { name: "Frank" };
const b = { name: "Frank" };

console.log(a === b); // false

Obwohl die beiden Objekte denselben Inhalt haben, sind sie zwei verschiedene Objekte.

Man kann sich vorstellen:

a ───────► { name: "Frank" }
           
b ───────► { name: "Frank" }

a und b zeigen auf unterschiedliche Objekte im Speicher.

Deshalb:

a === b // false

Derselbe Objektverweis

Wenn du dagegen ein Objekt kopierst, ohne ein neues Objekt zu erzeugen:

const a = { name: "Frank" };
const b = a;

console.log(a === b); // true

Jetzt:

a ─────┐
       ├──► { name: "Frank" }
b ─────┘

Beide Variablen referenzieren dasselbe Objekt.

Das ist referenzielle Gleichheit.

Funktionen und Arrays sind in JavaScript ebenfalls Objekte und unterliegen deshalb demselben Prinzip.

React verwendet an vielen Stellen referenzielle Vergleiche (Shallow Comparison), statt Objekte tief miteinander zu vergleichen (Deep Comparison). Das ist eine wichtige Performanceentscheidung.

Jeder Re-Render erzeugt eine neue Referenz obwohl die Werte oder die Funktionslogik gleich geblieben sind. Das kann zu einigen Problemen führen, die nachfolgend besprochen und gelöst werden. Das Lösungsprinzip ist dabei immer gleich: Wir stabilisieren die Referenz. In diesem Fall die Props von Komponenten, die mit React.memo() memoisiert sind.

Welche Aufgabe hat memo()?

Memo() hat die Aufgabe unnötiges Re-Rendern von langsamen Komponenten zu verhindern, besonders wenn diese oft gerendert werden. Mehr Aufgaben hat memo() nicht!

Wie erfüllt memo() seine Aufgabe?

memo() verhindert unnötige Re-Renders einer Komponente, indem es die Props zwischen dem aktuellen und dem vorherigen Render per shallow comparison vergleicht. Sind alle Props (auf oberster Ebene) referenziell bzw. wertgleich, überspringt React den Re-Render der Komponente komplett und behält den zuletzt gerenderten Output bei. Es ist damit ein reines Performance-Werkzeug, keine funktionale Notwendigkeit – die App verhält sich ohne memo() korrekt, nur potenziell langsamer.

Wann wird memo() verwendet?

Notwendig ist memo() nur, wenn eine Komponente häufig mit genau denselben Properties neu gerendert wird und wenn spürbare Performanceprobleme bestehen. Diese lassen sich mit dem React Profiler finden.

Wann wird memo() nicht verwendet?

Völlig nutzlos ist memo(), wenn die übergebenen Properties immer unterschiedlich sind und die Häufigkeit geänderter Properties nicht verringert werden kann. Wenn memo() keine signifikanten Performancevorteile bringt, dann hilft es entweder nicht oder es wurde falsch angewendet.

Wie wird memo() verwendet?

Memo ist eine Higher-Order-Funktion und erweitert die Funktion der Komponente die ihr als Argument übergeben wird. Memo prüft vor jedem Re-render ob sich die Properties ihrer Komponente geändert haben. Bei Änderungen wird ein Re-render durchgeführt, ansonsten nicht. Änderungen der Properties werden mit Object.is() geprüft. Primitive Datentypen wie String, Number und Boolean werden dabei über ihre Werte verglichen, nicht-primitive Datentypen wie Objekte, Arrays und Funktionen über die Referenz auf ihren Speicherplatz, also auf ihre Speicheradresse. Der Vergleich über Speicheradressen wird auch Shallow Comparison genannt und das Ergebnis dieses Vergleichs Referential Equality. Memo bietet zusätzlich auch Deep Comparison an, für die als zweiter Parameter eine Funktion übergeben wird, die Objekte und Arrays auf ihre Werte vergleicht.

memo(Component, arePropsEqual?)

Welche Nachteile hat memo()?

Die von Nachteile von memo() sind gering. Es gibt keine gravierenden Nachteile, jedoch wird die Lesbarkeit des Codes schlechter, dabei ist die Verwendung von memo() häufig sinnlos. Die Kosten für Performance sind nicht höher wie die Kosten eines Re-renders und die Kosten für den Speicherplatzverbrauch der Memoisierung sind sehr gering und damit bedeutungslos.

Memo macht übrigens genau das gleiche für funktionale React-Komponenten, wie PureComponent für klassenbasierte React-Komponenten. So viel zur Theorie, die Praxis wird jetzt viel spannender. Versprochen!

Ein beispielhaftes Performance Problem

Wir haben zwei Komponenten, eine ParentComponent mit einem Zähler und eine SlowChildComponent innerhalb der ParentComponent. Die SlowChildComponent hat ein künstlich erzeugtes Performanceproblem von 500 ms, welches auch beim Zähler in der ParentComponent zu einer eine Verzögerung von 500ms führt. Das ist ein klassisches User Experience Problem und muss bei einer realen Anwendung unbedingt beseitigt werden. Wie man das richtig macht, darum geht es in diesem Artikel. Doch zunächst das Problem zum anfassen:

export function ParentComponent() {
  let [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Click to increment count
      </button>
      <p>Count: {count}</p>
      <SlowChildComponent />
    </div>
  );
};

function SlowChildComponent() {
  let now = performance.now();
  while (performance.now() - now < 500) {
    /* performance problem */
  }
  return <h2>SlowChildComponent</h2>;
}
Live Code

Die Lösung mit memo()

Memo() hat die Aufgabe, Performanceprobleme durch langsame Komponenten zu lösen. Dazu übergibt man ihr die langsame Komponente als Argument, wie im folgenden Beispiel in Zeile 22:

code
export function ParentComponent() {
  let [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Click to increment count
      </button>
      <p>Count: {count}</p>
      <MemoSlowChildComponent />
    </div>
  );
};

function SlowChildComponent() {
  let now = performance.now();
  while (performance.now() - now < 500) {
    /* performance problem */
  }
  return <h2>SlowChildComponent</h2>;
})

const MemoSlowChildComponent = memo(SlowChildComponent)
Live Code

Probleme mit memo()

Memo() scheint so einfach zu sein, einfach jede Komponente mit memo umwickeln und Schwupp die Wupp gibt es keine unnötigen Re-render mehr. Leider nein! Ganz im Gegenteil! Wenn es so einfach wäre, hätte das React Team memo() schon längst standardmäßig implementiert. Deswegen gehe ich jetzt auf die Fallstricke bei der Verwendung von memo() ein. Ich zeige dir die Probleme und die Lösungen, damit du memo() richtig verwenden kannst.

Objekte, Arrays und Funktionen hebeln memo() aus

In meinem Artikel über Referential Equality bin ich schon darauf eingegangen. Nicht-primitiven Datentypen werden in JavaScript über ihre Speicheradressen verglichen, das Problem dabei, die Speicheradressen ändern sich bei jedem Re-render. Memo erkennt dadurch Änderungen in den Properties, obwohl sich nur die Speicheradresse nicht aber die Werte geändert haben. Es soll aber nur re-rendert werden, wenn sich mindestens ein Wert der Properties ändert. Das Ergebnis: Memo ist bei nicht-primitiven Datentypen als Properties fast unnütz! Auf das "fast" komme ich gleich wieder zurück.

<MemoSlowComponent someObjectProperty={{content: "I am an object"}} />

<MemoSlowComponent someArrayProperty={["I am an array"]} />

<MemoSlowComponent someFunctionProperty={() => "I am a function"} />

In diesen Live-Beispielen kannst die unterschiedlichen Datentypen und ihre Auswirkungen auf memo() testen.

LIVE CODE

Die Lösung useMemo und useCallback

Darf ich vorstellen, useMemo und useCallback - zwei Lösungen! Besser gesagt zwei mögliche Lösungen, denn für beide gibt es Alternativen, doch dazu gleich mehr. Zuerst useMemo und useCallback in Aktion. UseMemo wird zum memoisieren von Objekten und Arrays verwendet, useCallback zum memoisieren von Funktionen.

<MemoSlowComponent someObjectProperty={useMemo(() => {content: "I am an object"},[])} />

<MemoSlowComponent someArrayProperty={useMemo(() => ["I am an array"], [])} />

<MemoSlowComponent someFunctionProperty={useCallback(() => "I am a function", [])} />

LIVE CODE

Die Alternativen zu useMemo und useCallback

Die alternativen Lösungen hängen von der konkreten Situation ab. Letztendlich geht es jedoch immer um das gleiche Ziel, zu verhindern das sich Speicheradressen ändern, ohne das sich auch Werte geändert haben. Das ist das gleiche was useMemo und useCallback machen, nur im Idealfall zu geringeren Kosten. Geringer Kosten bedeuten eine bessere Lesbarkeit, weniger Rechenaufwand und weniger Speicherplatzverbrauch im Vergleich zur Memoisierung. Allerdings sind die Kosten von Memoisierung gering und die Verwendung von Alternativen sind häufig mehr eine Frage des Stils als eine Frage der Effizienz. Und ein guter Stils ist eben, die bestmögliche Lösung mit dem geringstmöglichen Aufwand abzuwägen. Nur gut das die Alternativen nicht Aufwendig sind, denn sonst müsstest du ja nie über Alternativen nachdenken! Das wäre ja fast schon wieder langweilig.

Im folgenden Beispiel wird das pet-Objekt an die memoisierte DisplayPet-Komponente übergeben. Memo würde dadurch unnütz, deswegen ist das pet-Objekt mit useMemo memoisiert. So weit nichts neues.

function Zoo() {
  const [name, setName] = useState("Lassie");
  const [species, setSpecies] = useState("dog");

  const pet = useMemo(
    () => ({ name, species }),
    [name, species]
  );

  return <DisplayPet pet={pet} />;
}

const DisplayPet = memo(function DisplayPet({ pet }) {
  // ...
});

Nur eine bessere Möglichkeit ist es hier, wenn wir auf das pet-Objekt ganz verzichten und die Werte "name" und "species" als Strings an die DisplayPet-Komponente übergeben. So sparen wir uns useMemo. Bei Bedarf, können wir das pet-Objekt dann in der DisplayPet-Komponente erstellen.

Werte als primitive Datentypen übergeben

function Zoo() {
  const [name, setName] = useState("Lassie");
  const [species, setSpecies] = useState("dog");

  return <DisplayPet name={name} species={species} />;
}

const DisplayPet = memo(function DisplayPet({ name, species }) {
  const pet = { name, species }  // if neccessary
  // ...
});

Für größere Objekte ist das natürlich nicht mehr sinnvoll und bisweilen auch unmöglich. Dafür kannst du prüfen ob nur einzelne Werte aus einem großen Objekt oder das ganze Objekt als Property übergeben werden müssen. Einzelne Werte kannst du dann mit Destructing aus dem Objekt ziehen und als primitive Datentypen übergeben.

Nur wirklich benötigte Werte übergeben

// const pet = { name, species } 

function Zoo({pet}) {
  const {species} = pet;  // destructing

  return <DisplaySpecies species={species} />;
}

const DisplaySpecies = memo(function DisplayPet({ species }) {
  // ...
});

Für Arrays gilt prinzipiell das gleiche, ganz besonders wenn es sich um Tuples handelt.

Funktionen auslagern

Eine Alternative für useCallback, die von den meisten vermutlich automatisch angewendet wird, ist das Auslagern von Funktionen aus Komponenten. Das funktioniert allerdings nur, wenn die Funktion keine Props oder State der Komponente benötigt.

function Zoo() {
  function hugPet() {
    // ...
  }

  // do something with hugPet()

  return <HappyPet hugPet={hugPet} />;
}

const HappyPet = memo(function HappyPet({ hugPet }) {
  // ...
});

UseCallback ist hier eine Option, wir können jedoch auch die Funktion hugPet() aus der Komponente herausnehmen, damit sie nicht mehr beim Re-rendern neu initialisiert wird. Das hat auch den Vorteil, dass hugPet() wiederverwendbar ist ohne sie als Property oder Context übergeben zu müssen.

// scripts.ts 
export function hugPet() {
   // ...
 }

// app.tsx
import {hugPet} from "./scripts"

function Life() {
  // do something with hugPet()

  return <HappyPet hugPet={hugPet} />;
}

const HappyPet = memo(function HappyPet({ hugPet }) {
  // ...
});

Je nach Programmlogik könnte auch die HappyPet-Komponente die Funktion hugPet() direkt aus der Datei scripts.ts verwenden. Zu Anschauungszwecken geht das im obigen Beispiel nicht.

Deep Comparison

Natürlich ist auch Deep Comparison eine Option um zumindest useMemo() zu vermeiden. Das kostet aber mehr CPU-Power und sollte daher nur in Ausnahmefällen verwendet werden. Sie ist dann sinnvoll, wenn die Props große oder tief verschachtelte Objekte/Arrays enthalten, deren Referenzen sich durch re-rendern häufig ändern, obwohl sich ihre Inhalte nicht geändert haben.

Auch Children sind Objekte oder Arrays!

Einfache HTML-Children sind Objekte

So weit so gut. Die Grundlagen sind gelegt. Spätestens jetzt wirst du die nachfolgenden Problemchen verstehen und nach der Lektüre dieses Artikels, hoffentlich beim coden beachten. Wie Kinder nun mal so sind, verursachen sie hier und da Trouble. Und wie Eltern nun mal so sind, liegt es nicht selten an ihnen, weil sie ihre Kids nicht richtig verstehen. Ganz ähnlich ist es auch bei React-Entwicklern, die gerne mal vergessen, das Children ganz normale Properties sind und als Einzelkinder, rein technisch betrachtet - JavaScript Objekte!

// HTML passed as a children
<MemoSlowComponent>
  <p>I am an HTML children</p>
</MemoSlowComponent>

// is exactly the same like this
<MemoSlowComponent children={<p>I am an HTML children</p>} />

// and that is kind like this
<MemoSlowComponent someObjectProperty={<p>I am an HTML children</p>} />

// The children property - its an object!
{$$typeof: Symbol(react.element), type: 'p', key: null, ref: null, props: Object, …}

Das Objekt des HTML-Children ist komplex und wird im ganzen benötigt. Alternativen wie Construction zur die Übergabe primitiver Datentypen gibt es daher nicht. Die einzige Lösung ist useMemo().

<MemoSlowComponent>
  {useMemo(() => <p>I am an HTML children</p>,[])}
</MemoSlowComponent>
Live Code

Einfache React-Children sind auch Objekte

Es macht auch keinen Unterschied ob es sich um HTML-Children oder um React-Komponenten als Children handelt. Solange es Einzelkinder sind, sind es Objekte!

// A React component passed as a children
<MemoSlowComponent>
  <ChildComponent />
</MemoSlowComponent>

// is exactly the same like this
<MemoSlowComponent children={<ChildComponent />} />

// and that is kind like this
<MemoSlowComponent someObjectProperty={<ChildComponent />} />

// The children property - its an object!
{$$typeof: Symbol(react.element), key: null, ref: null, props: Object, type: ƒ, …}

Auch das Objekt des Komponente ist komplex und wird im ganzen benötigt. Die einzige Lösung ist wieder useMemo().

<MemoSlowComponent>
  {useMemo(() => <ChildComponent />,[])}
</MemoSlowComponent>
LIVE CODE

Auch mit memo() memorisierte React-Children sind unmemoisierte Objekte

Memo() memoisiert Komponenten beim Rendern. Wird eine Komponente als Children und damit als Property übergeben, ist sie ein unmemoisiertes Objekt, das erst später gerendert wird.

// A with memo() memoized React component passed as a children
<MemoSlowComponent>
  <MemoChildComponent />
</MemoSlowComponent>

// is exactly the same like this
<MemoSlowComponent children={<MemoChildComponent />} />

// and that is kind like this
<MemoSlowComponent someObjectProperty={<MemoChildComponent />} />

// The children property - its an object!
{$$typeof: Symbol(react.element), type: Object, key: null, ref: null, props: Object, …}

Die einzige Lösung ist auch hier useMemo():

<MemoSlowComponent>
  {useMemo(() => <MemoChildComponent />,[])}
</MemoSlowComponent
Live Code

Mehrfache HTML- oder React-Children sind Arrays

Jede Node von HTML oder Komponenten als Children sind bilden einen Eintrag in einem Array. Das Problem dabei ist, das die Nodes nicht memoisiert werden können. Einzeln memoisiert bleiben sie ein Array und hebeln memo() aus und zusammen können sie nicht memoisiert werden da useMemo() mehrere Nodes als Argument nicht akzeptiert..

<MemoSlowComponent>
  <p>I am the first HTML children</p>
  <p>I am the second HTML children</p>
</MemoSlowComponent>

<MemoSlowComponent>
  <FirstChildComponent />
  <SecondChildComponent />
</MemoSlowComponent>

// The children property - its an array!
[Object, Object]

Die Verwendung von Fragments macht aus mehreren Nodes wie eine einzige Node, dass ganz normal mit useMemo() memoisiert werden kann.

<MemoSlowComponent>
  {useMemo(
    () => (
      <>  // use Fragments!
        <p>I am the first HTML children</p>
        <p>I am the second HTML children</p>
      </>
    ),[]
  )}
</MemoSlowComponent>

<MemoSlowComponent>
  {useMemo(
    () => (
      <>  // use Fragments!
        <FirstChildComponent />
        <SecondChildComponent />
      </>
    ),[]
  )}
</MemoSlowComponent>

Render Props sind Funktionen

Render Props können heute weitestgehend durch Hooks ersetzt werden, dennoch verwenden manche Entwickler sie auch heute noch wie eh und je. Auch moderne Libraries verwenden sie noch heute. Und natürlich gibt es auch noch jede Menge Legacy-Code mit Render Props. Render Props sie sind letztendlich Funktionen und hebeln wie jede andere Funktion memo() aus. Da sie auch heute noch verbreitet sind, sollte man auch sie im Bezug auf memo() auf dem Schirm haben.

// A React component passed as a render prop as children
<MemoSlowComponent>
  {() => <ChildComponent />}
</MemoSlowComponent>

// is exactly the same like this
<MemoSlowComponent children={() => <ChildComponent />} />

// and that is kind like this
<MemoSlowComponent someFunctionProperty={() => <ChildComponent />} />

// Render is traditionally used as the name for the property
<MemoSlowComponent render={() => <ChildComponent />} />
  • Wie bei allen Funktionen ist useCallback eine valide Lösung.

  • Auslagern geht nicht da Render Props, React-Komponenten sind die als Funktion übergeben werden, wodurch ihr Rendern kontrolliert werden kann.

  • Eine Alternative kann aber die Verwendung eines Hooks anstelle eines Render Props sein.

<MemoSlowComponent>
  {useCallback(() => <MemoChildComponent />,[])}
</MemoSlowComponent


<MemoSlowComponent render={useCallback(() => <MemoChildComponent />,[])} />
LIVE CODE

Vorsicht bei Props Spreading

Props Spreading ist kein direktes Problem. Wenn alle Properties primitive Datentypen sind oder alle nicht primitiven Properties memoisiert sind, dann wird memo() auch bei Props Spreading tadellos sein Aufgabe verrichten können. Das Problem dabei: Weißt du immer welche Datentypen in den Properties enthalten sind und ob diese gegebenenfalls memoisiert sind? Kontrollierst du das immer? Die Gefahr ist groß, dass dies vergessen wird! Hier ein Beispiel:

export const App = () => {
  return (
      <GrandparentComponent
        memoFunction={memoFunction}  // a function memoized with useCallback()
        memoObject={memoObject}  // an object memoized with useMemo()
      />
  );
};

export const GrandparentComponent = (props) => {
  return <ParentComponent {...props} badObject={{ warning: "bad" }} />
};

export const ParentComponent = (props) => {
  const { badObject } = props;
  return <ChildComponent {...props} />
};

export const ChildComponent = memo((props) => {
  return <span>BadObject breaks memo()</span>
});
LIVE CODE

Alternativen zu memo()

Beseitige die Ursache des Performanceproblems

Die beste Alternative für memo() ist es, langsame Komponenten zu vermeiden. Es gibt immer einen Grund warum eine Komponente langsam ist und dieser Grund kann gelegentlich beseitigt werden. Memo() ist wie eine Medizin die nur Symptome bekämpft, die dahinterliegende Ursache bleibt jedoch bestehen. Abgesehen davon hat memo() Nebenwirkungen wie schlechtere Lesbarkeit des Codes.

Optimierung durch Separations of Concerns

Frei von Nebenwirkungen ist diese Alternative für memo(). Der Zähler wird aus der ParentComponent, in eine eigene Komponente ausgelagert. Diese NewCounterComponent wird in der ParentComponent auf der gleichen Ebene wie die SlowChildComponent positioniert. Ein Re-render durch Hochzählen des Zählers führt nun nicht mehr zu einem Re-render der ParentComponent und dadurch auch zu einem Re-render der SlowChildComponent.

export function ParentComponent() {
  return (
    <div>
      <NewCounterComponent />
      <SlowChildComponent />
    </div>
  );
};

export function NewCounterComponent() {
  let [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Click to increment count
      </button>
      <p>Count: {count}</p>
    </div>
  );
}

function SlowChildComponent() {
  let now = performance.now();
  while (performance.now() - now < 500) {
    /* performane problem */
  }
  return <h2>SlowChildComponent</h2>;
}
LIVE CODE

Allerdings kann alles was überhalb der ParentComponent passiert auch weiterhin zum Re-rendern der SlowChildComponent führen. Diese Methode löst deswegen nicht immer das Performanceproblem, sinnvoll ist sie aber dennoch fast immer. Ihr Sinn ergibt sich aus dem Single Responsibility Prinzip und dem Prinzip der Separations of Concerns. Beides sind Begriffe und Methoden aus dem Clean Code. Die ParentComponent hat nur noch die eine Aufgabe die NewCounterComponent und die SlowChildComponent zu rendern und die NewCounterComponent hat nur die eine Aufgabe hochzuzählen (Single Responsibility Prinzip). Die ParentComponent trennt dabei die Logik des Zählers von seiner reinen Darstellung als NewCounterComponent (Separations of Concerns). Je nach Kontext kann gegebenenfalls auch die ParentComponent mir memo() memoisiert werden, wodurch mit diese Methode zumindest das Memo() in der SlowChildComponent gespart werden würde.

Optimierung durch das Elements as Children Pattern

Die folgende Methode hat mehrere Namen und ist frei von Nebenwirkungen. In den React Docs wird sie JSX as Children bezeichnet. Sie wird auch als Same Element Reference bezeichnet.

Wie das genau funktioniert hat Kent C. Dodds in seinem Post One simple trick to optimize React re-renders sehr genau beschreiben

export function GrandparentComponent() {
  return (
      <ParentComponent>
        <SlowChildComponent />
      </ParentComponent>
  );
}

function ParentComponent({ children }) {
  let [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Click to increment count
      </button>
      <p>Count: {count}</p>
      {children}
    </div>
  );
}

function SlowChildComponent() {
  let now = performance.now();
  while (performance.now() - now < 500) {
    /* performance problem */
  }
  return <h2>SlowChildComponent</h2>;
}
LIVE CODE

Auch diese Methode löst nicht in allen Fällen das Performanceproblem, denn alles was zum Re-rendern der GrandparentComponent führt wird auch weiterhin die SlowChildComponent rendern und dadurch zu Performaceproblemen führen. Dennoch ist es immer sinnvoll diese Methode zu verwenden, denn erstens gewöhnt man sich so an sie, zweitens schadet sie nicht und drittens wird sie dennoch Performance verbessern. Deswegen gilt auch hier: je nach Kontext GrandparentComponent mir memo() memoisiert werden, wodurch mit diese Methode zumindest das Memo() in der SlowChildComponent gespart werden würde.

Separations of Concerns & Elements as Children Pattern

Auch die Kombination beider Techniken ist frei von Nebenwirkungen und kann vielleicht noch das eine oder andere memo() mehr sparen. Allerdings kommt es auch hier wieder auf den Kontext an, denn das Re-render Problem wird letztendlich nur noch eine weitere Stufe nach oben verlagert, wo es dann bei Bedarf mit memo() behoben werden kann.

export function GrandparentComponent() {
  return (
      <ParentComponent>
        <SlowChildComponent />
      </ParentComponent>
  );
}

function ParentComponent({ children }) {
  return (
    <div>
      <NewCounterComponent />
      {children}
    </div>
  );
}

function NewCounterComponent() {
  let [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Click to increment count
      </button>
    </div>
  );
}

function SlowChildComponent() {
  let now = performance.now();
  while (performance.now() - now < 500) {
    /* performance problem */
  }
  return <h2>SlowChildComponent</h2>;
}

Der React Profiler als das Maß aller Dinge?

Ob memo() und seine Alternativen Sinn machen oder nicht, kann man glücklicherweise auch messen. Mit dem React Profiler kannst du die Performance von Komponenten mit und ohne memo() vergleichen. Ist der Unterschied nicht signifikant, dann bringen memo() und Alternativen auch nicht viel.

Zusammenfassung

Am Anfang habe ich geschrieben: Notwendig ist memo() nur, wenn eine Komponente häufig mit genau denselben Properties neu gerendert wird und wenn spürbare Performanceprobleme bestehen. Inzwischen ist klar, dass dies eine sehr oberflächliche Betrachtung ist. Aber sie bildet unseren Ausgangspunkt. Wo keine spürbaren oder messbaren Performanceprobleme bestehen, brauchen wir es nicht. Verwenden wir es trotzdem ist es nicht schlimm aber nicht optimal. Besteht ein erhebliches Performanceproblem, kennen wir nun verschiedene Lösungsoptionen wie wir memo() umgehen können und wie wir es richtig verwenden. Ein Vorher-Nachher-Vergleich mit dem React Profiler zeigt uns, ob richtig angewendetes memo() hilft oder nicht. Doch da langsame Komponenten auch beim ersten rendern langsam sind, ist das erste Ziel immer, das Performanceproblem direkt in der langsamen Komponente zu lösen. Erst wenn das nicht möglich ist, wird memo() zu einer Lösungsoption.