Пошук уроків, статей та іншого контенту
Кешування дорогих обчислень і стабільних посилань на функції — і коли це взагалі не потрібно.
Тіло функції-компонента виконується заново на кожен рендер — включно з усіма обчисленнями та створенням нових функцій усередині нього. Для більшості компонентів це абсолютно непомітно (мілісекунди), але для справді важких обчислень (сортування тисяч записів, складна агрегація) чи для функцій, переданих компонентам, обгорнутим у React.memo, повторна робота на кожен рендер може бути реальною проблемою.
useMemo запам'ятовує результат обчислення між рендерами й перераховує його заново лише тоді, коли змінюється щось із масиву залежностей:
function ProductList({ products, filterText }) {
const filtered = useMemo(() => {
console.log("Фільтрація виконується");
return products.filter((p) => p.name.includes(filterText));
}, [products, filterText]); // перерахунок лише при зміні products або filterText
return (
<ul>
{filtered.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
);
}Кожен рендер компонента створює нові функції-обробники з нуля — навіть якщо їхня логіка не змінилась, це нове посилання в пам'яті. Для React.memo-компонентів, які порівнюють props за посиланням, це означає марний перерендер: props технічно «змінились» (нова функція), хоча за змістом лишились тими самими. useCallback запам'ятовує саме посилання на функцію між рендерами:
function TodoList({ todos }) {
// Без useCallback: нова функція на кожен рендер TodoList
// З useCallback: те саме посилання, поки todos не зміниться
const handleToggle = useCallback((id) => {
setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}, []);
return todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
));
}
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
console.log("Рендер", todo.id);
return <li onClick={() => onToggle(todo.id)}>{todo.text}</li>;
});useCallback(fn, deps) — окремий випадок useMemo: useCallback(fn, deps) еквівалентний useMemo(() => fn, deps) — обидва кешують значення між рендерами, різниця лише в тому, що кешується (обчислений результат проти самої функції).
Мемоізація — оптимізація, а не типова практика «за замовчуванням». Для дешевих обчислень (простий map/filter на невеликому масиві, звичайна арифметика) накладні витрати на сам useMemo (порівняння масиву залежностей на кожному рендері) можуть перевищити виграш від кешування. Правильний порядок дій — написати простий код, виміряти реальну проблему продуктивності (наприклад, через React DevTools Profiler), і лише тоді додавати мемоізацію в конкретному вузькому місці.
Передчасна мемоізація «про всяк випадок» ускладнює код (кожен useMemo/useCallback вимагає стежити за коректністю масиву залежностей — та сама пастка застарілих значень, що й у useEffect) без гарантованого виграшу в продуктивності. Оптимізуйте виміряні вузькі місця, а не кожне обчислення підряд.
Обгортати useMemo/useCallback навколо дешевих обчислень без реального профілювання — додає складність без вимірного виграшу.
Неповний або неправильний масив залежностей у useMemo/useCallback — та сама пастка застарілого замикання, що й у useEffect, тільки тут вона повертає застарілий кешований результат замість актуального.
Використовувати useCallback без React.memo на дочірньому компоненті — стабільне посилання на функцію нічого не оптимізує, якщо дочірній компонент і так перерендериться через інші props.
useMemo кешує результат обчислення, useCallback кешує посилання на функцію — обидва перераховуються лише при зміні масиву залежностей. Це інструменти для конкретних, вимірених проблем продуктивності (дорогі обчислення, React.memo-компоненти, що марно перерендерюються через нові посилання на функції), а не практика, яку варто застосовувати до кожного обчислення в компоненті.