Откуда лишние рендеры
Когда компонент рендерится, React по умолчанию рендерит всех его потомков. Обычно это дёшево: рендер это вызов функций, а DOM меняется только там, где изменился результат. Проблемы начинаются, когда поддерево большое или в нём есть тяжёлые вычисления.
Прежде чем оптимизировать, измерьте: вкладка Profiler в React DevTools показывает, какие компоненты рендерились, почему и сколько это заняло. Замеряйте на продакшен-сборке, dev-режим заметно медленнее.
Сначала структура, потом мемоизация
Два приёма часто убирают проблему без единого memo.
Опустить state вниз. Если state нужен только полю ввода, вынесите поле с его state в отдельный компонент. Тогда ввод рендерит только этот компонент, а не всю страницу.
Передать тяжёлое как children. Элементы в children создаёт родитель. Когда меняется state обёртки, children остаётся той же ссылкой, и React его не рендерит заново.
function ScrollTracker({ children }) {
const [y, setY] = useState(0);
// ... подписка на scroll
return <div>{y}{children}</div>; // children не перерендерятся при смене y
}
<ScrollTracker><HeavyTree /></ScrollTracker>
memo
memo(Component) пропускает рендер, если все props равны прошлым по Object.is (поверхностное сравнение). Работает, только если props действительно стабильны:
const Row = memo(function Row({ item, onSelect }) { ... });
<Row item={item} onSelect={() => select(item.id)} /> // новая функция каждый рендер: memo бесполезен
Ломают memo: стрелочные функции и объекты-литералы в props, style={{...}}, JSX в children. Изменение context или собственного state рендерит memo-компонент в любом случае.
useMemo и useCallback
useMemo(() => compute(a, b), [a, b]) кеширует результат и пересчитывает его, только если изменились зависимости.
useCallback(fn, deps) кеширует саму функцию. Это то же, что useMemo(() => fn, deps).
Когда это имеет смысл:
- Вычисление действительно дорогое (заметные миллисекунды на сортировку и фильтрацию большого массива).
- Значение передаётся в
memo-компонент, и без стабильной ссылки мемоизация ребёнка не работает. - Значение стоит в зависимостях эффекта или другого хука.
useCallback без memo у получателя ничего не даёт: функция всё равно создаётся, а ребёнок рендерится. Мемоизация не бесплатна: сравнение зависимостей и хранение кеша на каждом рендере, плюс шум в коде.
Зависимости должны быть полными, иначе получите устаревшее значение.
React Compiler
React Compiler (стабильная версия 1.0 с октября 2025) это плагин сборки, который автоматически мемоизирует компоненты и значения внутри них. С ним ручные memo, useMemo и useCallback в большинстве случаев не нужны. Условие: код соблюдает правила React (чистый рендер, без мутаций props и state, правила хуков). Компоненты, где правила нарушены, компилятор пропускает. В Next.js и Vite компилятор подключается в конфигурации.
Другие инструменты
- Виртуализация длинных списков (TanStack Virtual, react-window): рендерить только видимые строки.
useTransitionиuseDeferredValue: пометить тяжёлое обновление как несрочное, чтобы ввод оставался отзывчивым.- Ленивая загрузка через
lazyиSuspense: меньше JavaScript на старте.
Что спросят на собесе
- Почему компонент рендерится, если его props не поменялись.
- Чем
useMemoотличается отuseCallbackи когда они бесполезны. - Почему
memoне сработал (инлайн-функция, объект, children). - Как найти причину медленного интерфейса.
- Что делает React Compiler и нужна ли теперь ручная мемоизация.