React

Мемоизация: memo, useMemo, useCallback

Таблица на 5000 строк. Разработчик печатает в поле фильтра над ней, и каждый набранный символ подвешивает интерфейс на 200 миллисекунд. Профайлер показывает: при каждом нажатии React заново рендерит всю таблицу, хотя её данные не изменились. Виноват не React и не таблица, а одна функция-обработчик, которая пересоздаётся при каждом рендере и тянет за собой пересчёт всего поддерева. Эта история - про три инструмента, которые до выхода React Compiler были единственным способом её починить вручную: memo, useMemo и useCallback.

  • Финансовые дашборды (Bloomberg-подобные терминалы): тысячи строк котировок, где лишний ре-рендер таблицы при каждом тике цены недопустим
  • Редакторы вроде Figma и Notion: тяжёлые деревья компонентов, где мемоизация отсекает пересчёт неизменившихся панелей
  • Списки в соцсетях: виртуализированные ленты оборачивают элементы в memo, чтобы скролл не дёргался
  • Карты и графики (data-viz): дорогие вычисления координат кэшируются через useMemo, иначе каждый рендер пересчитывает геометрию
  • До React Compiler v1.0 (2025) почти любой крупный проект на React содержал сотни ручных useMemo и useCallback

Предварительные знания

  • Понимание, что React вызывает функцию-компонент заново при изменении state или props
  • Reconciliation: как React сравнивает деревья и решает что обновить в DOM
  • Разница между равенством по ссылке и равенством по значению в JavaScript

Откуда взялась ручная мемоизация в React

Идея пропускать работу, если входные данные не изменились, стара как программирование - это классическая мемоизация из теории вычислений. В React она появилась слоями. Сначала был shouldComponentUpdate в классовых компонентах и PureComponent с поверхностным сравнением props. Когда в React 16.6 (2018) добавили React.memo, тот же приём пришёл в функциональные компоненты. Хуки useMemo и useCallback прибыли вместе со всеми хуками в React 16.8 (февраль 2019). С тех пор и до 2025 года расстановка этих обёрток была обязательным навыком: разработчик вручную подсказывал React, где можно не пересчитывать. React Compiler v1.0 эту ручную работу почти отменил, но понимать механику по-прежнему необходимо.

Корень проблемы: референсное равенство

Чтобы понять, зачем нужны три инструмента мемоизации, надо начать с одного факта о JavaScript. Примитивы (числа, строки, булевы) сравниваются по значению: 5 равно 5. Объекты, массивы и функции сравниваются по ссылке: два объекта с одинаковым содержимым не равны, если это разные объекты в памяти. React при проверке props использует поверхностное сравнение - проходит по ключам и сравнивает значения через Object.is. Для примитива это работает интуитивно, для объекта - нет.

Теперь ключевой момент. Тело функции-компонента выполняется заново при каждом рендере. Каждый объектный литерал, каждый массив и каждая стрелочная функция внутри тела создаются заново - это новые ссылки. Если такое значение передаётся в дочерний компонент как prop, поверхностное сравнение всегда видит 'props изменились', даже когда содержимое то же самое.

В примере выше каждый клик по кнопке пересоздаёт style и handleClick. Child получает формально новые props при каждом рендере Parent, и любая попытка его мемоизировать окажется бесполезной, пока эти ссылки не стабилизированы. Это и есть та самая корневая причина, которую решают useMemo и useCallback.

Почему объект, созданный внутри тела компонента, считается новым prop при каждом рендере?

memo и useMemo: пропустить рендер и кэшировать значение

React.memo - это обёртка вокруг компонента. Она запоминает последние props и при следующем рендере родителя сравнивает новые props со старыми поверхностно. Если все они равны, React пропускает вызов этого компонента и переиспользует прошлый результат. Без memo компонент ре-рендерится всегда, когда ре-рендерится его родитель, независимо от props.

useMemo решает другую задачу - кэширует результат вычисления. Первый аргумент это функция, которая что-то считает, второй массив зависимостей. React вызывает функцию только когда одна из зависимостей изменилась, иначе возвращает запомненное значение. Два применения: дорогие вычисления (сортировка большого массива, тяжёлый парсинг) и стабилизация ссылки на объект или массив, который передаётся в memo-компонент.

  • memo — Работает на уровне компонента. Решает, вызывать ли компонент вообще, сравнивая его props. Оборачивает определение компонента.
  • useMemo — Работает на уровне значения внутри компонента. Решает, пересчитывать ли конкретное выражение, сравнивая массив зависимостей.

useMemo гарантирует кэш только как оптимизацию, а не как семантику. React оставляет за собой право иногда сбросить кэш (например, для освобождения памяти). Поэтому код не должен зависеть от того, что значение никогда не пересчитается - useMemo это подсказка, а не контракт хранения.

Компонент обёрнут в memo, но всё равно ре-рендерится при каждом рендере родителя. Какая причина наиболее вероятна?

useCallback и вопрос: когда это вообще нужно

useCallback - это частный случай useMemo для функций. Запись useCallback(fn, deps) эквивалентна useMemo(() => fn, deps). Он возвращает ту же самую ссылку на функцию между рендерами, пока зависимости не изменятся. Сам по себе useCallback ничего не ускоряет: его смысл проявляется только когда стабильная функция передаётся в memo-компонент или используется как зависимость другого хука.

Без useCallback здесь handleSelect был бы новой функцией при каждом рендере Parent, и memo вокруг Row стал бы бесполезен - все строки ре-рендерились бы при любом изменении selected. С useCallback ссылка стабильна, и React пропускает рендер строк, у которых не изменился item. Это связка: memo на ребёнке плюс стабильные props через useCallback и useMemo.

Главная ошибка - мемоизировать всё подряд. Каждый useMemo и useCallback сам по себе тратит память на хранение и время на сравнение зависимостей. Если ребёнок не обёрнут в memo, useCallback вокруг переданной ему функции не даёт ничего, кроме накладных расходов. Преждевременная мемоизация делает код шумным и иногда медленнее, чем без неё.

  1. Сначала измерить: открыть React DevTools Profiler и убедиться, что ре-рендеры действительно проблема
  2. Мемоизировать, когда дочерний компонент обёрнут в memo и получает объект или функцию как prop
  3. Мемоизировать значение, чьё вычисление измеримо дорого (большая сортировка, фильтрация, парсинг)
  4. Не мемоизировать дешёвые значения и компоненты, которые и так почти не рендерятся
  5. В новых проектах рассмотреть React Compiler, который снимает большую часть этой ручной работы

С React Compiler v1.0 (2025) большинство ручных memo, useMemo и useCallback становятся избыточными: компилятор анализирует код и расставляет эквивалентную мемоизацию сам. Но он работает только при соблюдении Rules of React, и понимание ручной механики остаётся базой для чтения чужого кода и отладки производительности.

Разработчик оборачивает каждую функцию-обработчик в проекте в useCallback, хотя дочерние компоненты не обёрнуты в memo. К чему это приведёт?

Связь с другими темами

Этот урок открывает блок про производительность. Дальше:

  • React Compiler — Компилятор сам расставляет мемоизацию на этапе сборки, делая ручные обёртки почти ненужными
  • Reconciliation — Фундамент под мемоизацией: именно процесс сравнения деревьев решает, дойдёт ли работа до компонента
  • useTransition — Другой подход к отзывчивости: не пропустить работу, а понизить её приоритет

Итог

  • memo оборачивает компонент и пропускает его ре-рендер, если props поверхностно равны предыдущим
  • useMemo кэширует результат дорогого вычисления между рендерами, пока не изменятся зависимости
  • useCallback кэширует саму функцию, сохраняя её ссылку стабильной между рендерами
  • Корень большинства лишних ре-рендеров - референсное равенство: новый объект, массив или функция при каждом рендере ломает поверхностное сравнение
  • Мемоизация не бесплатна: она тратит память и сравнения, поэтому оправдана при доказанной проблеме, а не превентивно

Связанные уроки

  • rc-17-reconciliation — Мемоизация имеет смысл только когда понятно, как React решает что и когда ре-рендерить через reconciliation
  • rc-20-react-compiler — React Compiler автоматизирует ровно ту работу, которую этот урок учит делать руками
  • rc-13-useref — useRef тоже про сохранение значения между рендерами, но без участия в реактивности
Мемоизация: memo, useMemo, useCallback

0

1

Войти