Dlaczego React.memo nadal ponownie renderuje: tożsamość referencji w propsach
Zobacz, jak React.memo porównuje właściwości, dlaczego funkcje wewnątrz innych funkcji, obiekty i tablice go pokonują, jak ustabilizować referencje oraz dlaczego profilowanie powinno poprzedzać proces memoizacji.
React.memo jest często traktowany jako narzędzie do poprawy wydajności: otaczając komponent tym elementem, renderowanie powtarzalne znika. W praktyce aplikacje pełne memo mogą nadal wydawać się powolne, ponieważ wolna interfejs użytkownika w React zazwyczaj wynika ze struktury komponentów, niestabilnych właściwości, kosztownych obliczeń lub po prostu z nadmiernego renderowania, a nie z braku otaczającego elementu. Ten przewodnik pokazuje, co tak naprawdę porównuje memo, wzorce właściwości, które potajemnie go unieważniają, oraz jak zdecydować, czy w ogóle warto go używać.
Czego faktycznie sprawdza memo
memo nie powstrzymuje renderowania komponentu. Porównuje nowe właściwości z poprzednimi, jedną po drugiej, za pomocą Object.is, i pomija renderowanie tylko wtedy, gdy wszystkie są identyczne. Zmiany we własnym stanie komponentu lub w kontekście, z którego czerpie dane, nadal powodują jego renderowanie. Właściwości prymitywne, takie jak łańcuchy znaków, liczby i wartości logiczne, są porównywane według ich wartości, więc działają naturalnie.
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>
);
});
Jeśli name i age pozostają niezmienione, React może ponownie wykorzystać poprzedni wynik. Problemy pojawiają się przy funkcjach, obiektach i tablicach, ponieważ w tym przypadku Object.is porównuje referencje, a nie zawartość.
Kallbacki wewnątrz komponentu są tworzone na nowo przy każdym renderowaniu
Tutaj rodzic przekazuje funkcję strzałkową do komponentu przypominanego za pomocą memo.
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>
);
});
Każde renderowanie App tworzy nową funkcję. Jej treść jest identyczna, ale odniesienie do niej się różni, więc memo interpretuje to jako zmianę właściwości i za każdym razem renderuje CounterButton.
Rozwiązaniem jest utrzymanie stabilnego odniesienia za pomocą useCallback. Ponieważ używana jest forma aktualizacji prev => prev + 1, tablica zależności może pozostać pusta.
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>
);
};
Zauważ, że w tym fragmencie CounterButton nie jest już otoczony przez memo. Bez tego otoczenia element potomny jest renderowany na nowo razem ze swoim rodzicem, a useCallback nie przynosi żadnego efektu. Oba rozwiązania są skuteczne tylko razem: należy zachować memo(...) wokół elementu potomnego z poprzedniego przykładu. Nie należy również dodawać useCallback wszędzie; ma on swoje koszty i powinien być używany tam, gdzie rozwiązuje konkretny problem.
Obiekty i tablice w formacie inline zachowują się w ten sam sposób
Literal obiektu znajdujący się wewnątrz ciała komponentu jest tworzony na nowo przy każdym renderowaniu.
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>
);
});
Podobnie jest z tablicą opcji.
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>
);
});
Oba przypadki generują nowe referencje za każdym razem, przez co ich zapisane w pamięci dzieci są ponownie renderowane.
Przenoś dane, które się nigdy nie zmieniają
Gdy dane są statyczne, przenieś je do zakresu modułu, aby zostały utworzone tylko raz. Lista opcji może znajdować się poza komponentem.
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;
Jak widać, pierwszy element odnosi się do zmiennej role, której nie ma w zakresie modułu, więc ten kod wywołuje błąd ReferenceError. Należy użyć literalu takiego jak { label: "Admin", value: "admin" }. Jeśli element rzeczywiście zależy od propów lub stanu, należy przechowywać tablicę wewnątrz komponentu i otoczyć ją funkcją useMemo, podając te wartości jako zależności (prawdopodobnie dlatego właśnie importowano useMemo).
Obiekt konfiguracji jest traktowany w ten sam sposób.
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>
);
});
Mierz przed użyciem memoizacji
Korzystne pytania pojawiają się przed każdym analizatorem: który komponent jest faktycznie wolny, dlaczego się renderuje, czy ten proces renderowania jest kosztowny oraz czy można zmniejszyć obciążenie zamiast je ukrywać. Profiler w React DevTools odpowiada na większość z nich, pokazując, które komponenty zostały renderowane, dlaczego, ile czasu zajęło to każdemu z nich oraz które aktualizacje były niepotrzebne. Lista kontrolna na wykonywania mniej operacji przed użyciem memoizacji rozwija tę kolejność działań.
Główne wnioski
memojest przydatny tylko wtedy, gdy każda właściwość zachowuje stabilną referencję.- Funkcje, obiekty, tablice i wartości kontekstu zdefiniowane inline regularnie unieważniają jego działanie.
useCallbackbez zmemorizowanego potomka to zmarnowany wysiłek.- Zachowaj
useMemona kosztowne obliczenia lub referencje, które muszą pozostać stabilne.