Більшість туторіалів з продуктивності React закінчуються однією й тією ж поганою порадою: обгорніть усе в useMemo та useCallback — і готово. Якщо ви дотримувалися цієї поради, ви, ймовірно, лише сповільнили свій додаток. Ці хуки не є безкоштовними. Кожен із них виділяє пам'ять, порівнює залежності та зберігає кешовані значення. Використані без мети, вони стають зайвим навантаженням замість оптимізації.
Давайте відкинемо зайве і зосередимося на тому, що справді важливо.
Що насправді робить кожен хук
useMemo запам'ятовує значення. Ви передаєте йому функцію, яка виконує важку роботу, і він повертає результат. Під час наступного рендеру, якщо ваші залежності не змінилися, React пропускає обчислення і повертає старий результат.
useCallback запам'ятовує функцію. Він не виконує функцію за вас. Він просто повертає той самий екземпляр функції між рендерами, поки її залежності залишаються незмінними.
У цьому полягає вся різниця. Один кешує обчислене значення. Інший кешує посилання. Плутанина між ними призводить до коду, який виглядає оптимізованим, але поводиться так само, як і неогорнутий код, при цьому споживаючи додаткову пам'ять.
Чому ідентичність функцій руйнує ваше дерево компонентів
Коли компонент перерендериться, React знову виконує все тіло функції. Кожна змінна створюється заново. Кожна інлайнова функція отримує абсолютно нову адресу в пам'яті.
У JavaScript дві функції, що містять ідентичну логіку, не є рівними. () => {} === () => {} повертає false. Те саме правило стосується об'єктів та масивів. Якщо ваш батьківський компонент визначає handleSubmit і передає її дочірньому компоненту, цей нащадок отримуватиме новий проп при кожному рендері. Навіть якщо дочірній компонент обгорнутий у React.memo, він не зможе зрозуміти, що нова функція робить те саме, що й стара. Посилання змінилося, тому дочірній компонент перерендериться.
Це і є першопричина, для вирішення якої було створено useCallback. Йдеться не про швидкість, а про стабільність.
Коли useMemo виправдовує себе
useMemo потрібен тоді, коли ви виконуєте обчислення, які є об'єктивно дорогими, і ви помічаєте відчутне запізнення.
Уявіть фільтрацію величезного набору даних. Якщо у вас є таблиця з десятками тисяч рядків і поле пошуку, всередині компонента ви можете написати щось подібне:
const visibleRows = rows.filter(r => r.name.includes(query));
Без useMemo цей цикл виконується при кожному рендері. Якщо користувач натисне кнопку, яка перемикає бічну панель, батьківський компонент перерендериться, і ваш фільтр запуститься знову, навіть якщо rows та query не змінилися. На великих наборах даних це заїкання буде помітним.
useMemo виправляє це, фіксуючи результат:
const visibleRows = useMemo(() => {
return rows.filter(r => r.name.includes(query));
}, [rows, query]);
Тепер React перезапускає цей фільтр лише тоді, коли залежності дійсно змінюються.
Та сама логіка застосовується до складних математичних обчислень, перетворення відповідей API у формати, придатні для графіків, або отримання стану, який інакше постійно перераховувався б заново.
Є ще один, менш очевидний випадок використання. Якщо ви створюєте об'єкт або масив локально і додаєте його в масив залежностей useEffect, ви можете випадково викликати цей ефект при кожному рендері. Інлайнові об'єкти та масиви щоразу отримують нову ідентичність, тому ефект бачить змінену залежність і спрацьовує знову. Мемоїзація цього об'єкта за допомогою useMemo зберігає стабільність посилання та дозволяє ефекту спрацьовувати лише тоді, коли вихідні дані дійсно змінюються.
Коли useCallback стає необхідним
useCallback має найбільше значення, коли ви передаєте обробники в дочірні компоненти, оптимізовані за допомогою React.memo.
Уявіть батьківський компонент, який містить лічильник. Він також рендерить «дорогий» дочірній список:
function Parent() {
const [count, setCount] = useState(0);
const handleItemClick = (id) => {
console.log(id);
};
return (
<div>
<button onClick={() => setCount(c + 1)}>{count}</button>
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
Щоразу, коли змінюється count, Parent перерендериться. Створюється нова функція handleItemClick. Оскільки ExpensiveList отримує нове посилання на проп, він також перерендериться. Якщо ExpensiveList обгорнутий у React.memo, ця мемоїзація стає абсолютно марною, оскільки проп-функція змінилася.
useCallback зберігає посилання:
const handleItemClick = useCallback((id) => {
console.log(id);
}, []);
Тепер ExpensiveList перерендериться лише тоді, коли це дійсно необхідно.
Ще одна критична ситуація стосується useEffect. Якщо ефект підписується на функцію, визначену всередині вашого компонента, і ця функція змінює свою ідентичність при кожному рендері, ефект буде постійно видалятися та підписуватися знову. Мемоїзація функції робить ефект стабільним.
Пастка масиву залежностей та застарілі замикання
Обидва хуки покладаються на масиви залежностей, і саме тут ховається більшість багів.
If you omit a variable from the dependency array, your memoized function or value closes over an old version of that variable. This is a stale closure. The UI might display fresh data, but your callback is still looking at state from three renders ago. The fix is simple but easy to miss during code reviews: include every value used inside the hook that could change.
Run the react-hooks/exhaustive-deps ESLint rule. It will catch obvious omissions. But do not treat it as a robot. Understand why each dependency matters.
The Hidden Tax of Over-Optimization
Beginners often armor every function and every value with these hooks because it feels safe. That habit backfires.
React must store cached values in memory. On every render, it must iterate through your dependency array and compare each item using Object.is. That comparison is cheap, but it is not free. If you wrap a trivial event handler like onClick={() => setOpen(true)} inside useCallback, you are paying memory and CPU costs to avoid creating a function that would have been instant to allocate.
The hooks also add noise. Code wrapped in useMemo and useCallback is harder to read and harder to maintain. Every dependency array is a potential stale closure waiting to bite you.
The real rule of thumb is unglamorous but effective: write plain code first. Optimize only when you have proof of a problem. Use the React DevTools Profiler to identify which components are expensive and which renders are wasteful. If a render takes less than a few milliseconds, no user will notice, and your memoization is solving nothing.
The Bottom Line
useMemo is for expensive values. useCallback is for stable function references. Neither hook makes your component render faster on its own; they prevent unnecessary downstream work. Start without them, measure with actual tooling, and add them precisely where the profiler shows a bottleneck. Clean code that rerenders occasionally will almost always beat over-engineered code that memoizes everything.
