Чому React.memo продовжує перерендеруватися: ідентичність посилання в пропсах
Дізнайтеся, як React.memo порівнює пропси, чому вбудовані функції, об’єкти та масиви підривають його ефективність, як стабілізувати посилання та чому профілювання має передувати мемоїзації.
React.memo часто сприймається як засіб для покращення продуктивності: достатньо обгорнути компонент, і перерендеринг зникає. На практиці додатки, наповнені елементами memo, все одно можуть працювати повільно, оскільки повільна робота інтерфейсу React зазвичай спричинена структурою компонентів, нестабільними параметрами, витратними обчисленнями або просто надмірним кількістю елементів, які потрібно відобразити, а не відсутністю обгортки. У цьому посібнику розглядається, з чим насправді порівнює memo, які шаблони параметрів потайки підривають його ефективність, та як вирішити, чи варто його взагалі використовувати.
Що насправді перевіряє memo
memo не заважає компоненту відображатися. Він порівнює нові параметри з попередніми, по одному за разом, за допомогою Object.is, і пропускає процес відображення лише тоді, коли всі вони ідентичні. Зміни у власному стані компонента чи у контексті, який він читає, все одно змушують його відображатися. Примітивні параметри, такі як рядки, числа та логічні значення, порівнюються за значенням, тому вони працюють природним чином.
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>
);
});
Якщо name та age залишаються незмінними, React може повторно використати попередній результат. Проблеми виникають з функціями, об’єктами та масивами, оскільки для них Object.is порівнює посилання, а не вміст.
Інлайн-колбеки створюються заново щоразу під час відображення
Тут батьківський компонент передає функцію-стрілку до кнопки, яка має оптимізацію через 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>
);
});
Кожне відтворення App створює нову функцію. Її тіло є ідентичним, але посилання відрізняється, тому memo сприймає змінену властивість та щоразу відтворює CounterButton.
Рішення полягає у збереженні стабільного посилання за допомогою useCallback. Оскільки використовується форма оновлення prev => prev + 1, масив залежностей може залишатися порожнім.
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>
);
};
Зверніть увагу, що в цьому фрагменті CounterButton більше не оточений memo. Без цього обгортки дитина перевідтворюється разом із своїм батьком у будь-якому випадку, і useCallback не має жодного ефекту. Обидва елементи є корисними лише разом: потрібно зберегти memo(...) навколо дитини з попереднього прикладу. Також не варто додавати useCallback скрізь; він має свою вартість та підходить лише там, де вирішує конкретну проблему.
Об’єкти та масиви типу inline поводяться однаково
Літерал об’єкта всередині тіла компонента створюється заново при кожному відображенні.
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>
);
});
Те саме стосується й масива опцій.
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>
);
});
Обидва варіанти створюють нові посилання щоразу, тому їхні запам’ятовані дочірні елементи знову відображаються.
Перемістіть дані, які ніколи не змінюються
Якщо дані є статичними, перемістіть їх у межі модуля, щоб вони створювалися лише один раз. Список опцій може знаходитися поза компонентом.
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;
Як видно з виводу, перший елемент посилається на змінну role, якої немає у межах модуля, тому цей код викидає помилку ReferenceError. Використовуйте літерал, наприклад { label: "Admin", value: "admin" }. Якщо елемент дійсно залежить від параметрів чи стану, залиште масив усередині компонента та обгорніть його функцією useMemo, вказавши ці значення як залежності (ймовірно, саме тому імпортується useMemo).
Об’єкт конфігурації піддається такій самій обробці.
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>
);
});
Вимірюйте перед тим, як використовувати memoization
Корисні запитання виникають ще до того, як ми почнемо аналіз: який компонент насправді працює повільно, чому він відображається, чи є цей процес ресурсоемним, та чи можна скоротити обсяг роботи замість того, щоб її приховувати. React DevTools Profiler відповідає на більшість з них, показуючи, які компоненти були відображені, чому, скільки часу це зайняло та які оновлення були зайвими. Чек-ліст у про те, як виконувати менше операцій перед мемоїзацією деталізує цей порядок дій.
Основні висновки
memoдопомагає лише тоді, коли кожен проп зберігає стабільний посилання.- Лінійні функції, об’єкти, масиви та значення контексту часто унеможливлюють його ефективну роботу.
useCallbackбез мемоізованого дочірнього елемента є марною тратою зусиль.- Використовуйте
useMemoлише для ресурсоемких обчислень чи посилань, які мають залишатися стабільними.