Пошук уроків, статей та іншого контенту
React перерендерює компоненти набагато частіше, ніж здається — розбираємось, коли мемоізація реально допомагає, а коли лише ускладнює код без користі.
Мемоізація — це збереження результату обчислення або посилання на функцію, щоб не створювати їх заново під час кожного рендеру.
У React для цього є два основні хуки:
useMemo — кешує результат обчислення;
useCallback — кешує посилання на функцію.
const value = useMemo(() => calculateValue(data), [data]);
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);Важливо: ці хуки не зупиняють рендер компонента самі по собі. Вони лише допомагають зберігати значення або функції між рендерами.
Компонент рендериться знову, коли:
змінюється його state;
змінюються props;
оновлюється батьківський компонент;
змінюється значення контексту, яке він використовує;
компонент примусово оновлюється.
Якщо батьківський компонент рендериться, React зазвичай повторно викликає і його дочірні компоненти. Це не обов’язково означає, що браузер перемалює весь DOM: React порівняє нове дерево з попереднім і застосує лише необхідні зміни.
Наприклад:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
{count}
</button>
<Child />
</>
);
}
function Child() {
console.log('Child render');
return <div>Дочірній компонент</div>;
}Після кожного натискання кнопки Parent рендериться знову. Разом із ним React викликає Child, навіть якщо його результат не змінився.
Сам факт повторного виклику компонента ще не є проблемою. Рендери в React мають бути достатньо дешевими. Оптимізація потрібна тоді, коли є вимірювана причина: важкі обчислення, великі списки або зайві рендери дорогих піддерев.
useMemo: кешування результатуuseMemo приймає функцію та масив залежностей:
const cachedValue = useMemo(() => {
return calculateSomething(value);
}, [value]);Функція виконується під час рендеру. Якщо залежності не змінилися, React повертає попередній результат.
function ProductList({ products, query }) {
const filteredProducts = useMemo(() => {
// Фільтруємо список лише тоді, коли змінилися товари або пошуковий запит
return products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase())
);
}, [products, query]);
return (
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Без useMemo фільтрація запускалася б під час кожного рендеру ProductList, навіть якщо змінився, наприклад, лише стан відкриття меню.
У цьому прикладі мемоізація може бути корисною, якщо:
products містить багато елементів;
фільтрація або сортування справді займає помітний час;
компонент часто рендериться з інших причин.
Для маленького масиву проста фільтрація зазвичай не потребує useMemo.
useMemo не робить обчислення асинхроннимuseMemo не переносить роботу у фоновий потік і не робить її автоматично швидшою. Обчислення все одно виконується під час рендеру, якщо змінилася хоча б одна залежність.
Також useMemo не призначений для побічних ефектів:
// Неправильне використання
const result = useMemo(() => {
saveDataToServer(data);
return transform(data);
}, [data]);Запити, підписки, зміни DOM та інші побічні ефекти потрібно виконувати через useEffect або спеціальні механізми бібліотеки.
useCallback: кешування функціїФункції в JavaScript — це об’єкти. Кожен запис із оголошенням функції створює нове посилання:
const first = () => {};
const second = () => {};
console.log(first === second); // falseУ React це має значення, коли функція передається дочірньому компоненту:
function Parent() {
const handleSelect = (id) => {
console.log(id);
};
return <Child onSelect={handleSelect} />;
}Під час кожного рендеру Parent створюється нова функція handleSelect. Для звичайного дочірнього компонента це часто не має значення. Але якщо Child обгорнутий у React.memo, нове посилання на функцію змусить його рендеритися знову.
const Child = memo(function Child({ onSelect }) {
console.log('Child render');
return <button onClick={() => onSelect(1)}>Вибрати</button>;
});Щоб посилання на функцію залишалося стабільним, можна використати useCallback:
function Parent() {
const [count, setCount] = useState(0);
const handleSelect = useCallback((id) => {
console.log(id);
}, []);
return (
<>
<button onClick={() => setCount(count + 1)}>
{count}
</button>
<Child onSelect={handleSelect} />
</>
);
}Тепер handleSelect не створюється заново між рендерами, доки не зміняться його залежності.
useCallback часто потрібен разом із React.memoСам по собі useCallback не запобігає рендеру компонента:
function Parent() {
const handleClick = useCallback(() => {
console.log('click');
}, []);
return <Child onClick={handleClick} />;
}Якщо Child не мемоізований і батьківський компонент рендериться, Child усе одно буде викликаний знову.
Практична схема зазвичай виглядає так:
Дочірній компонент обгортають у memo.
Функції, які передаються йому через props, стабілізують за допомогою useCallback.
Значення-об’єкти або масиви, які передаються йому, стабілізують за допомогою useMemo.
const UserPanel = memo(function UserPanel({ user, onSave }) {
return (
<section>
<h2>{user.name}</h2>
<button onClick={onSave}>Зберегти</button>
</section>
);
});
function Profile({ user }) {
const handleSave = useCallback(() => {
console.log('Профіль збережено');
}, []);
const viewModel = useMemo(() => {
return {
name: user.name,
};
}, [user.name]);
return <UserPanel user={viewModel} onSave={handleSave} />;
}Але така оптимізація має сенс лише тоді, коли UserPanel справді часто рендериться без необхідності або його рендер дорогий.
useMemo справді потрібенЦе найочевидніший випадок:
сортування великих масивів;
складна фільтрація;
побудова великих структур даних;
обчислення статистики;
перетворення даних, яке помітно навантажує процесор.
const chartData = useMemo(() => {
return buildChartData(transactions);
}, [transactions]);Перед додаванням хука варто перевірити, чи саме buildChartData створює проблему. Якщо функція виконується за долі мілісекунди, кешування може бути зайвим.
memouseMemo може бути потрібен не через вартість обчислення, а через стабільність посилання:
const options = useMemo(() => {
return {
theme: 'dark',
pageSize: 20,
};
}, []);
return <DataGrid options={options} />;Без useMemo об’єкт options створювався б заново під час кожного рендеру. Для React.memo це були б нові props, навіть якщо поля об’єкта мають ті самі значення.
Іноді стабільне посилання потрібне для коректної роботи ефекту:
const options = useMemo(() => {
return {
serverUrl,
roomId,
};
}, [serverUrl, roomId]);
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]);Проте в багатьох випадках краще створити об’єкт безпосередньо всередині ефекту. Це зменшує кількість залежностей і робить код простішим:
useEffect(() => {
const options = {
serverUrl,
roomId,
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);useCallback справді потрібенЦе основний випадок:
const SearchButton = memo(function SearchButton({ onSearch }) {
return <button onClick={onSearch}>Пошук</button>;
});
function SearchForm() {
const [query, setQuery] = useState('');
const handleSearch = useCallback(() => {
console.log('Пошук:', query);
}, [query]);
return <SearchButton onSearch={handleSearch} />;
}Тут handleSearch залежить від query, тому його посилання має змінюватися після зміни запиту. Це правильно: функція повинна бачити актуальне значення.
function ChatRoom({ roomId }) {
const createOptions = useCallback(() => {
return {
roomId,
serverUrl: 'https://example.com',
};
}, [roomId]);
useEffect(() => {
const options = createOptions();
connectToRoom(options);
return () => disconnectFromRoom(options);
}, [createOptions]);
return <div>Кімната: {roomId}</div>;
}Однак тут також часто можна спростити код, створивши функцію всередині useEffect:
function ChatRoom({ roomId }) {
useEffect(() => {
const options = {
roomId,
serverUrl: 'https://example.com',
};
connectToRoom(options);
return () => disconnectFromRoom(options);
}, [roomId]);
return <div>Кімната: {roomId}</div>;
}Якщо функція не передається дочірньому компоненту і не потрібна як стабільна залежність, useCallback може бути непотрібним.
const fullName = `${firstName} ${lastName}`;Немає сенсу обгортати таке вираження в useMemo. Вартість виклику useMemo, перевірки залежностей і збереження значення може бути більшою за вартість самого обчислення.
const Child = memo(function Child({ user }) {
return <div>{user.name}</div>;
});
function Parent({ name }) {
const user = {
name,
};
return <Child user={user} />;
}Child все одно рендеритиметься, бо user — новий об’єкт на кожному рендері. Якщо стабільність цього об’єкта справді потрібна, його можна мемоізувати:
const user = useMemo(() => ({ name }), [name]);Але ще простіше — передавати примітивне значення:
return <Child name={name} />;Якщо функція використовується лише в тому самому компоненті і не є залежністю ефекту, її мемоізація зазвичай не дає користі:
function Form() {
const handleSubmit = useCallback(() => {
console.log('submit');
}, []);
return <form onSubmit={handleSubmit} />;
}Для такого сценарію звичайна функція часто достатня:
function Form() {
const handleSubmit = () => {
console.log('submit');
};
return <form onSubmit={handleSubmit} />;
}Мемоізація не є безкоштовною. React повинен:
зберігати попереднє значення або функцію;
порівнювати залежності під час кожного рендеру;
підтримувати додаткову логіку в компоненті;
зберігати значення в пам’яті, доки воно потрібне.
Крім технічної вартості, є ще вартість для розробника:
складніше читати код;
легше помилитися із залежностями;
важче зрозуміти, чому певний компонент оптимізований;
додатковий код може приховати справжню проблему продуктивності.
useMemo і useCallback — це оптимізаційні підказки, а не спосіб зробити будь-який компонент швидшим.
Не варто додавати мемоізацію лише через припущення. Перевірте:
що саме рендериться;
скільки часу займає рендер;
чи виконується дороге обчислення повторно;
чи справді дитячий компонент рендериться без потреби.
Для цього можна використовувати React DevTools Profiler і звичайні вимірювання часу.
console.time('filter products');
const filteredProducts = products.filter((product) =>
product.name.includes(query)
);
console.timeEnd('filter products');Таке вимірювання допомагає зрозуміти реальну вартість операції, а не покладатися на інтуїцію.
Перед useMemo та useCallback часто допомагають простіші рішення:
перенести стан ближче до компонентів, які його використовують;
розділити великий компонент на менші;
не створювати об’єкти та масиви без потреби;
передавати примітиви замість об’єктів;
винести статичні значення за межі компонента;
уникати зайвих ефектів, які запускають додаткові оновлення.
Не потрібно мемоізувати всі функції та значення в компоненті. Знайдіть конкретну операцію або конкретний дочірній компонент, який створює проблему.
Масив залежностей має містити всі значення, які використовуються всередині функції та можуть змінюватися:
const result = useMemo(() => {
return calculate(price, taxRate);
}, [price, taxRate]);Якщо пропустити залежність, можна отримати застаріле значення:
// Небезпечно: taxRate використовується, але відсутня серед залежностей
const result = useMemo(() => {
return calculate(price, taxRate);
}, [price]);Подібна проблема стосується і useCallback:
const handleSave = useCallback(() => {
saveUser(userId, name);
}, [userId, name]);Не варто додавати порожній масив залежностей лише для того, щоб функція «ніколи не змінювалася». Якщо функція використовує змінні з компонента, вони мають бути враховані.
const title = useMemo(() => 'Профіль', []);
const handleClick = useCallback(() => {}, []);Такі конструкції рідко дають користь. Вони лише збільшують обсяг коду.
useCallback без React.memoЯкщо дочірній компонент не порівнює props за допомогою memo або іншого механізму, стабільне посилання на функцію часто нічого не змінює.
Це може призвести до використання застарілих props або state. Лінтер із правилами для хуків зазвичай допомагає виявити такі помилки, і його попередження не варто бездумно ігнорувати.
useMemo та useCallbackconst value = useMemo(() => calculate(), []);
const callback = useCallback(() => calculate(), []);useMemo повертає результат calculate();
useCallback повертає саму функцію.
Фактично useCallback(fn, deps) можна уявити як спеціальний випадок мемоізації функції, але ці хуки мають різне призначення в коді.
Якщо компонент рендериться занадто часто через зайві оновлення стану або ефекти, useMemo не усуне першопричину. Спочатку потрібно зрозуміти, чому відбуваються ці оновлення.
Мемоізоване значення не слід сприймати як незмінне сховище даних або механізм збереження стану. Це оптимізація обчислення. Логіка компонента не повинна ламатися, якщо React повторно виконає функцію мемоізації.
Перед використанням хука поставте собі кілька запитань:
Чи є тут обчислення, яке реально займає багато часу?
Чи рендериться компонент достатньо часто, щоб це мало значення?
Чи передається об’єкт, масив або функція мемоізованому дочірньому компоненту?
Чи є це значення залежністю useEffect або іншого хука?
Чи виміряв я проблему до і після оптимізації?
Чи не можна вирішити проблему простішою структурою компонента?
Якщо на всі запитання відповідь «ні», мемоізація, найімовірніше, не потрібна.
useMemo кешує результат обчислення.
useCallback кешує посилання на функцію.
Обидва хуки потрібні для конкретних проблем, а не як обов’язковий шаблон кожного компонента.
useCallback найчастіше має сенс разом із React.memo.
useMemo корисний для дорогих обчислень і стабільних об’єктів або масивів.
Дешеві операції не потрібно мемоізувати.
Залежності мають бути повними й актуальними.
Спочатку вимірюйте проблему, потім оптимізуйте.
Часто простіша структура компонентів ефективніша за велику кількість мемоізації.