Startseite / Artikel

Warum React.memo weiterhin neu rendernt: Referenzidentität in den Props

Sehen Sie, wie React.memo Eigenschaften vergleicht, warum inline-Funktionen, Objekte und Arrays ihn ausschalten, wie Referenzen stabilisiert werden können und warum Profilierung vor der Memoisierung kommen sollte.

Verlag: REACTAPP.TOP5 Min. Lesezeit1092 Wörter

Quellen: React.memo: Useful, but Often Misused

React.memo wird oft als Leistungs-Schalter angesehen: Man umhüllt einen Komponenten und die erneute Darstellung entfällt. In der Praxis können Apps, die voller memo-Komponenten sind, dennoch träge wirken, denn langsame React-UIs entstehen in der Regel durch die Komponentenstruktur, instabile Props, aufwändige Berechnungen oder einfach zu viel Darstellung – nicht durch das Fehlen einer Umhüllung. Diese Anleitung zeigt, was memo tatsächlich vergleicht, die Props-Muster, die es heimlich wirkungslos machen, sowie wie man entscheiden kann, ob es überhaupt sinnvoll ist, es zu verwenden.

Was memo tatsächlich überprüft

memo verhindert nicht, dass eine Komponente gerendert wird. Es vergleicht die neuen Props mit den vorherigen, Prop für Prop, mithilfe von Object.is, und überspringt die Renderung nur dann, wenn alle gleich sind. Änderungen am eigenen Zustand der Komponente oder an einem Context, den sie liest, führen dennoch zu einer Renderung. Primitive Props wie Zeichenketten, Zahlen und Boolesche Werte werden nach ihrem Wert verglichen, sodass sie problemlos funktionieren.

import { memo } from "react";

const App = () => {
  const name = "John";
  const age = 25;

  return <UserCard name={name} age={age} />
};

const UserCard = memo(({ name, age }) => {
  return (
    <div>
      <h3>{name}</h3>
      <p>Age: {age}</p>
    </div>
  );
});

Falls name und age unverändert bleiben, kann React die vorherige Ausgabe wiederverwenden. Probleme entstehen bei Funktionen, Objekten und Arrays, denn für diese vergleicht Object.is die Referenzen und nicht den Inhalt.

Inline-Callbacks werden bei jeder Renderung neu erstellt

Hier übermittelt ein Elternteil eine Pfeilfunktion an einen mit memo versehenen Button.

import React, { useState, memo } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>Count: {count}</h2>

      <CounterButton
        onIncrement={() => setCount((prev) => prev + 1)}
      />
    </div>
  );
};

const CounterButton = memo(({ onIncrement }) => {
  return (
    <button onClick={onIncrement}>
      Increment
    </button>
  );
});

Jede Auflösung von App erzeugt eine neue Funktion. Ihr Körper ist identisch, doch die Referenz unterscheidet sich, wodurch memo eine veränderte Eigenschaft erkennt und daher jedes Mal CounterButton neu rendern lässt.

Die Lösung besteht darin, die Referenz mit useCallback stabil zu halten. Da die Aktualisierungsformel prev => prev + 1 verwendet wird, kann der Abhängigkeitsarray leer bleiben.

import React, { useState, useCallback } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return (
    <div>
      <h2>Count: {count}</h2>

      <CounterButton
        onIncrement={handleIncrement}
      />
    </div>
  );
};

const CounterButton = ({ onIncrement }) => {
  return (
    <button onClick={onIncrement}>
      Increment
    </button>
  );
};

Beachten Sie, dass in diesem Auszug CounterButton nicht mehr von memo umschlossen ist. Ohne diese Umhüllung wird das Kind unabhängig vom Elternteil neu gerendert, und useCallback bringt nichts. Beides ist nur gemeinsam sinnvoll: Behalten Sie memo(...) um das Kind aus dem vorherigen Beispiel. Fügen Sie außerdem useCallback nicht überall hinzu; es hat seinen eigenen Aufwand und sollte nur dort eingesetzt werden, wo es ein konkretes Problem löst.

Inline-Objekte und Arrays verhalten sich auf dieselbe Weise

Ein Objektliteral innerhalb des Komponentenkörpers wird bei jeder Renderung neu erstellt.

import { memo } from "react";

const App = () => {

  const config = {
    theme: "dark",
    pageSize: 20,
    showFilters: true,
  };

  return (
    <div>
      <DataGrid config={config} />
    </div>
  );
};

const DataGrid = memo(({ config }) => {
  return (
    <div>
      <h3>Theme: {config.theme}</h3>
      <p>Page Size: {config.pageSize}</p>
    </div>
  );
});

Dasselbe gilt für ein Array mit Optionen.

import { memo } from "react";

const App = () => {
  const options = [
    { label: "Admin", value: "admin" },
    { label: "Editor", value: "editor" },
    { label: "User", value: "user" },
  ];

  return (
    <div>
      <Select options={options} />
    </div>
  );
};

const Select = memo(({ options }) => {

  return (
    <select>
      {options.map((option) => (
        <option key={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
});

Sowohl Objekte als auch Arrays erzeugen jedes Mal neue Referenzen, wodurch ihre gememorisierten Kinder erneut rendern werden.

Daten, die sich niemals ändern, nach oben verschieben

Falls die Daten statisch sind, sollten sie in den Modulscope verschoben werden, damit sie nur einmal erstellt werden. Die Liste der Optionen kann außerhalb der Komponente liegen.

import { useState, useMemo, memo } from "react";

const options = [
      { label: role.toUpperCase(), value: role },
      { label: "Editor", value: "editor" },
      { label: "User", value: "user" },
    ]

const App = () => {

  return (
    <div>
      <Select options={options} />
    </div>
  );
};

const Select = memo(({ options }) => {
  return (
    <select>
      {options.map((option) => (
        <option key={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
});

export default App;

Wie abgedruckt, bezieht sich der erste Eintrag auf eine role-Variable, die im Modulumfang nicht existiert, wodurch dieser Code einen ReferenceError auslöst. Verwenden Sie einen Literal wie { label: "Admin", value: "admin" }. Wenn ein Eintrag tatsächlich von Props oder State abhängt, halten Sie das Array innerhalb der Komponente und umschließen es mit useMemo, wobei diese Werte als Abhängigkeiten angegeben werden (was vermutlich der Grund ist, warum useMemo importiert wird).

Auch das Konfigurationsobjekt erhält dieselbe Behandlung.

import { memo } from "react";

const config = {
  theme: "dark",
  pageSize: 20,
  showFilters: true,
};

const App = () => {
  return (
    <div>
      <DataGrid config={config} />
    </div>
  );
};

const DataGrid = memo(({ config }) => {

  return (
    <div>
      <h3>Theme: {config.theme}</h3>
      <p>Page Size: {config.pageSize}</p>
    </div>
  );
});

Messen, bevor man memoisiert

Nützliche Fragen stehen immer zuerst: Welcher Komponente fehlt es tatsächlich an Leistung, warum wird sie gerendert, ist diese Renderung aufwändig, und kann die Arbeit reduziert werden anstatt versteckt? Der React DevTools Profiler beantwortet die meisten davon, indem er zeigt, welche Komponenten gerendert wurden, warum, wie lange jede davon gebraucht hat und welche Aktualisierungen unnötig waren. Die Checkliste zu mehr Arbeit zu vermeiden, bevor man Memoisierung einsetzt erweitert diese Reihenfolge der Schritte.

Haupterkenntnisse

  • memo hilft nur, solange jede Eigenschaft eine stabile Referenz behält.
  • Inliner-Funktionen, Objekte, Arrays und Context-Werte verhindern dies in der Regel.
  • useCallback ohne eine gememoisierte Kindkomponente ist sinnlose Mühe.
  • Reservieren Sie useMemo für aufwändige Berechnungen oder Referenzen, die stabil bleiben müssen.
  • Die Virtualisierung langer Listen sowie die Aufteilung großer Komponenten bringt in der Regel mehr Vorteile als das Memoisieren.
  • Analysieren Sie zuerst den Profilverlauf; lassen Sie die Daten entscheiden.