React-мемоизация: когда useMemo и memo вредят
Мемоизация в React не бесплатна: обёртки съедают память и время сравнения пропсов. Разбираем, когда useMemo, useCallback и memo реально ускоряют рендер, а когда только замедляют.

Компонент обёрнут в memo, а перерисовывается на каждый клик
Типичная картина: карточка товара обёрнута в React.memo, коллбэки — в useCallback, вычисления — в useMemo. По ощущению «оптимизировано под завязку». Профайлер React DevTools показывает, что список из 200 карточек всё равно перерисовывается целиком при каждом изменении родительского state. Мемоизация не сработала, а расходы на неё остались: React на каждый рендер вызывает функцию сравнения пропсов, держит в памяти прошлые значения и замыкания.
React 18–19, function-компоненты, хуки. Речь именно о ручной мемоизации — useMemo, useCallback, React.memo. Автоматическую оптимизацию через React Compiler (бывший React Forget) вынесем в конец отдельным абзацем: он меняет расклад, но на конец 2024 года остаётся в состоянии beta.
Что мемоизация делает на самом деле
Три инструмента решают три разные задачи, и путаница между ними — источник половины бесполезных обёрток.
- React.memo — обёртка над компонентом. Пропускает повторный рендер, если пропсы поверхностно равны прошлым. Работает на уровне «рендерить дочерний компонент или нет».
- useMemo — кеширует результат вычисления между рендерами, пока не изменятся зависимости. Экономит CPU на самом вычислении.
- useCallback — частный случай useMemo для функций. Возвращает ту же ссылку на функцию, пока зависимости не поменялись.
Ключевой момент: useCallback и стабильная ссылка на объект нужны почти всегда только для того, чтобы React.memo дочернего компонента сработал. Сами по себе они рендер не ускоряют. Если ребёнок не обёрнут в memo, стабилизировать переданный ему коллбэк бессмысленно — компонент перерисуется в любом случае.
Мемоизация коллбэка без memo на принимающей стороне — это оплата страховки от аварии, которая физически не может произойти. Ссылку вы стабилизировали, но её никто не сравнивает.
Почему обёртка ломается чаще, чем кажется
Самая частая причина «memo не работает» — среди пропсов есть значение, которое пересоздаётся на каждом рендере родителя. Поверхностное сравнение по ссылке считает их разными.
// Родитель. React 18
function ProductList({ items }) {
const [query, setQuery] = useState('');
return (
<ul>
{items.map((item) => (
<ProductCard
key={item.id}
item={item}
// Новый объект на каждый рендер — memo у ProductCard бесполезен
style={{ padding: 8 }}
// Новая функция на каждый рендер — то же самое
onSelect={() => console.log(item.id)}
/>
))}
</ul>
);
}
const ProductCard = React.memo(function ProductCard({ item, style, onSelect }) {
return <li style={style} onClick={onSelect}>{item.name}</li>;
});Здесь style и onSelect создаются заново при каждом рендере ProductList. Даже если item не менялся, две другие пропсы всегда «новые», и memo впустую сравнивает их и всё равно перерисовывает карточку. К расходам на сам рендер добавились расходы на сравнение.
Как чинить
Либо убрать нестабильные пропсы, либо стабилизировать их — но осознанно:
style={{ padding: 8 }}вынести в константу вне компонента:const CARD_STYLE = { padding: 8 };— объект создаётся один раз на модуль.- Коллбэк, зависящий от
item.id, лучше не создавать на каждую строку. Передавайтеitemвнутрь и вызывайте обработчик там, либо используйте один общийonSelect, а id доставайте из data-атрибута.
Когда мемоизация вредит
Каждая обёртка не бесплатна. У неё есть постоянная цена, которую платят даже когда оптимизация не срабатывает.
- Память.
useMemoиuseCallbackдержат прошлое значение и массив зависимостей до следующего рендера. На тысяче элементов это тысяча замыканий. - CPU на сравнение.
React.memoпри каждом потенциальном рендере сравнивает все пропсы. Если пропсов много и они всё равно меняются — вы добавили работу, а не убрали. - Читаемость и баги. Забытая зависимость в массиве
useMemoдаёт устаревшее значение (stale closure) — ошибка, которую тяжело поймать, потому что «иногда работает».
Дешёвое вычисление мемоизировать вредно. Сравните стоимость самого вычисления с накладными расходами.
| Вычисление в useMemo | Стоит ли | Почему |
|---|---|---|
a + b, конкатенация строк | Нет | Само вычисление дешевле, чем сравнение зависимостей и хранение результата |
| Фильтрация/сортировка массива на 1000+ элементов | Да | Вычисление реально дорогое, повтор на каждый рендер заметен |
| Создание объекта/массива, передаваемого в memo-ребёнка | Да | Стабильная ссылка нужна, чтобы дочерний memo сработал |
| Создание объекта, который никуда дальше не идёт | Нет | Референс никто не сравнивает — экономить нечего |
Правило: сначала измерь, потом оборачивай
Не мемоизируйте «на всякий случай». Порядок действий, который работает:
- Откройте React DevTools → Profiler, включите запись, повторите действие, которое тормозит.
- Найдите компоненты с большим временем рендера или те, что перерисовываются без причины (в настройках профайлера есть «Highlight updates» и подсветка причин рендера).
- Убедитесь, что проблема именно в рендере, а не в тяжёлом вычислении в обработчике или в сетевом ожидании. Профайлер покажет, где именно уходит время.
- Только для найденного узкого места примените точечную мемоизацию: стабилизируйте пропсы дорогого
memo-компонента или закешируйте конкретное тяжёлое вычисление. - Замерьте снова. Если разницы нет — откатите обёртку.
Замеры делайте в production-сборке или как минимум понимайте, что dev-режим React медленнее и цифры в нём завышены. Порядок величин важнее абсолютных значений: если рендер списка занимал 40 мс, а стал 6 мс на том же наборе данных — оптимизация оправдана. Если было 3 мс и стало 2 — вы потратили время на шум.
Где эта статья заканчивается
Виртуализация длинных списков, разбиение bundle и useTransition для несрочных обновлений — отдельные темы. Здесь мы говорили только про ручную мемоизацию рендера.
React Compiler меняет расклад
React Compiler (ранее React Forget) автоматически расставляет мемоизацию на этапе сборки, анализируя код. Идея в том, чтобы useMemo, useCallback и memo руками писать почти не приходилось. На конец 2024 года компилятор в статусе beta и подключается как плагин к сборке. Если он у вас включён — ручные обёртки в большинстве случаев становятся лишними и мешают компилятору. Проверяйте текущий статус и требования к коду в официальной документации, потому что API и правила меняются между релизами.
Что делать дальше
Снимите профайл на реальном тормозящем сценарии, а не на предположении. Найдите два-три компонента с наибольшим суммарным временем рендера и работайте с ними точечно. Проверьте, есть ли в проекте memo-компоненты, которым передают инлайн-объекты и стрелочные функции, — это первый кандидат на бесполезную обёртку. И следите за статусом React Compiler: когда он выйдет в stable, часть этой ручной работы уйдёт в сборку.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: React — useMemo (официальная документация), React — memo (официальная документация)
Частые вопросы
У меня React 17, а не 18 — приёмы работают?
Я обернул компонент в memo, а он всё равно перерисовывается. Почему?
Стоит ли на всякий случай оборачивать в useCallback все функции?
Как понять, что мемоизация реально помогла, а не навредила?
Если у меня уже включён React Compiler, нужно убирать ручные useMemo?
Мемоизация помогает от тормозов при вводе в поле?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.