useMemo и useCallback: равенство ссылок
Урок про useMemo и useCallback: что такое равенство ссылок, когда мемоизация нужна, а когда только вредит.
useMemo кэширует значение между рендерами, useCallback кэширует функцию. Оба возвращают одну и ту же ссылку, пока не изменятся зависимости.
Корень всего — равенство ссылок
В JavaScript объекты, массивы и функции сравниваются по ссылке, а не по содержимому. Два «одинаковых» объекта — разные значения. Прочувствуем это в чистом JS — пример запускаемый:
const a = { x: 1 };
const b = { x: 1 };
const c = a;
console.log("a === b:", a === b); // разные объекты
console.log("a === c:", a === c); // та же ссылка
function makeAdder() {
return (n) => n + 1;
}
const f1 = makeAdder();
const f2 = makeAdder();
console.log("f1 === f2:", f1 === f2); // новые функции каждый вызов
// Симуляция: "рендер" каждый раз создаёт новый объект пропсов
function render() {
return { style: { color: "red" } };
}
console.log("props равны?", render().style === render().style);
Вывод:
a === b: false a === c: true f1 === f2: false props равны? false
Вот почему наивно переданные пропсом { color: "red" } или () => ... каждый рендер «новые» и ломают React.memo. useMemo/useCallback возвращают ту же ссылку, пока зависимости не поменялись.
useMemo — для дорогих вычислений и стабильных значений
function Report({ rows }) {
// пересчитается только при изменении rows, а не на каждый рендер
const total = React.useMemo(
() => rows.reduce((s, r) => s + r.amount, 0),
[rows]
);
return <p>Итого: {total}</p>;
}
Две законные причины для useMemo: (1) вычисление реально тяжёлое (тысячи элементов, сложная агрегация); (2) результат — объект/массив, который вы передаёте в мемоизированного ребёнка или в зависимости другого хука, и вам нужна стабильная ссылка.
useCallback — стабильная функция
useCallback(fn, deps) — это, по сути, useMemo(() => fn, deps). Нужен, когда функцию передают в React.memo-ребёнка или указывают в зависимостях useEffect: без него ссылка на функцию меняется каждый рендер.
const handleSelect = React.useCallback((id) => {
setSelected(id);
}, []); // ссылка стабильна, memo-ребёнок не перерисуется зря
Когда они ВРЕДНЫ
Мемоизация не бесплатна: React хранит прошлое значение и массив зависимостей и сравнивает их каждый рендер. Если «оптимизировать» дешёвое — вы добавили работу и шум, ничего не выиграв.
useMemoвокругa + bилиarr.length— бессмысленно: само вычисление дешевле проверки зависимостей.useCallbackдля функции, которую вы передаёте в обычный (не memo) DOM-элемент, — пустая трата:<button onClick={fn}>и так не выигрывает от стабильной ссылки.- Лишние мемоизации замусоривают код и прячут реальные узкие места.
Правило большого пальца: тянитесь к useMemo/useCallback, когда (а) вычисление дорогое, либо (б) ссылка нужна стабильной для React.memo-ребёнка или зависимостей хука. В остальных случаях — не нужно.
Итог
- Объекты/массивы/функции сравниваются по ссылке — каждый рендер создаёт новые.
useMemoкэширует значение,useCallback— функцию, до смены зависимостей.- Применяйте для дорогих расчётов и для стабильных ссылок в memo/effect.
- Не мемоизируйте дешёвое — проверка зависимостей сама стоит времени.