useMemo и useCallback

Знакомимся с хуками оптимизации: useMemo кеширует значение, useCallback — функцию. И учимся не злоупотреблять ими.

useMemo запоминает результат вычисления между рендерами; useCallback запоминает саму функцию. Оба пересчитывают значение только при смене зависимостей.

Проблема: лишние вычисления

При каждой перерисовке тело компонента выполняется заново. Если внутри есть тяжёлое вычисление, оно повторяется даже когда исходные данные не менялись:

function ProductList({ products, query }) {
  // выполняется при КАЖДОЙ перерисовке, даже если products не менялся
  const filtered = products.filter((p) => p.name.includes(query));
  return <List items={filtered} />;
}

useMemo: кеш результата

useMemo сохраняет результат и пересчитывает его только при изменении зависимостей:

import { useMemo } from "react";

function ProductList({ products, query }) {
  const filtered = useMemo(
    () => products.filter((p) => p.name.includes(query)),
    [products, query] // пересчёт только при их смене
  );
  return <List items={filtered} />;
}

Пока products и query те же — React вернёт запомненный массив, не вычисляя заново.

useCallback: кеш функции

При каждой перерисовке функции, объявленные в компоненте, создаются заново — это новые объекты. Обычно это неважно, но если функция передаётся в оптимизированный дочерний компонент, новая ссылка заставит его перерисоваться зря. useCallback сохраняет ту же функцию между рендерами:

import { useCallback } from "react";

function Parent({ items }) {
  const handleSelect = useCallback((id) => {
    console.log("Выбран", id);
  }, []); // та же функция между рендерами

  return <ChildList items={items} onSelect={handleSelect} />;
}

По сути useCallback(fn, deps) — это сокращение для useMemo(() => fn, deps).

Стабильность ссылок на чистом JS

Чтобы понять, почему «новая функция = новая ссылка» вообще проблема, посмотрите, как сравниваются функции и объекты в JS:

function makeFn() {
  return () => "привет";
}
const a = makeFn();
const b = makeFn();
console.log(a === b);   // разные объекты-функции → false
const same = a;
console.log(a === same); // та же ссылка → true

Вывод:

false
true

Каждый «новый» объект или функция — это новая ссылка (false). Оптимизированные компоненты сравнивают пропсы по ссылке, поэтому новая функция выглядит для них как «изменившийся проп». useCallback возвращает ту же ссылку — и лишней перерисовки не происходит.

Не оптимизируйте вслепую

Главная ошибка — оборачивать в useMemo/useCallback всё подряд. Это само по себе стоит памяти и усложняет код. Применяйте их только когда:

  • вычисление действительно тяжёлое, или
  • функция/значение передаётся в оптимизированный (мемоизированный) дочерний компонент.

В большинстве случаев React и так достаточно быстр — преждевременная оптимизация лишь вредит читаемости.

Итог

  • useMemo кеширует результат вычисления, useCallback — саму функцию; оба обновляются по зависимостям.
  • Польза — избежать тяжёлых пересчётов и лишних перерисовок мемоизированных детей из-за новых ссылок.
  • Не оборачивайте в них всё подряд: применяйте точечно, иначе только усложните код.
Проверьте себя
1. Что кеширует useMemo?
AСаму функцию
BРезультат вычисления, пересчитывая его только при смене зависимостей
CDOM-узел
DСостояние компонента
2. Зачем нужен useCallback?
AЧтобы кешировать результат функции
BЧтобы сохранить ту же ссылку на функцию между рендерами и избежать лишних перерисовок
CЧтобы вызвать функцию немедленно
DЧтобы заменить useState
3. Когда НЕ стоит применять useMemo/useCallback?
AКогда вычисление действительно тяжёлое
BОборачивая в них всё подряд без реальной необходимости
CКогда значение передаётся в мемоизированный дочерний компонент
DНикогда не стоит ограничивать их использование
Поддержать проект