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.
  • Не мемоизируйте дешёвое — проверка зависимостей сама стоит времени.
Проверьте себя
1. Как в JavaScript сравниваются объекты и функции?
AПо содержимому поля за полем
BПо ссылке: два «одинаковых» объекта — разные значения
CПо длине в байтах
DВсегда равны, если поля совпадают
2. Чем useCallback отличается от useMemo?
AuseCallback кэширует функцию, useMemo — значение
BНичем, это синонимы
CuseCallback работает только в классах
DuseMemo нельзя использовать с зависимостями
3. Когда useMemo вреден?
AВокруг тяжёлой агрегации тысяч элементов
BВокруг дешёвого выражения вроде a + b
CКогда результат передают в memo-ребёнка
DКогда нужна стабильная ссылка для useEffect
Поддержать проект