React.memo vs useMemo: когда и как использовать
Как правильно применять React.memo и useMemo для ускорения рендеров в таблицах, фильтрах и пагинации. Примеры кода и чеклист.
Введение
Если ваш список начал лагать, а консоль полна предупреждений о “лишних” перерисовках, вы уже знаете, что нужно искать оптимизацию. Часто в такой момент первым на ум приходит React.memo или useMemo. Они оба обещают “меньше работы”, но работают по-разному. Ошибки в их применении приводят к тому, что код становится громоздким, а выигрыша нет.
Разобрать, что где лучше ставить, полезно не только для больших таблиц. Это касается любой интерактивной UI-страницы: формы, дашборды, модальные окна. Понимание различий помогает писать чистый код и избежать “мемо-переусложнения”, когда мемоизация только усложняет поддержку.
Рассмотрим реальную задачу компонент-таблицу с фильтрами и сортировкой и шаг за шагом разберем, где уместен React.memo, а где useMemo. К концу вы сможете выбирать инструмент, а не бросать оба сразу.
1. Что мемоизирует React.memo?
React.memo это HOC (Higher-Order Component), который сравнивает текущие props с предыдущими. Если они “равны” (shallow compare), React пропустит перерисовку дочернего компонента.
import React from "react";
const Row = React.memo(({ item }) => {
console.log("Row render", item.id);
return <div>{item.title}</div>;
});
| Плюс | Минус |
|---|---|
| Сокращает количество рендеров при неизменных пропсах | Не помогает, если пропсы часто меняются по-ссылке |
| Легко добавить к любой функции-компоненту | Требует тщательной проверки props (deep сравнение дорого) |
Когда использовать:
- Компонент получает простые, неизменные пропсы (примитивы, объекты, которые не пере-создаются каждый рендер).
- Перерисовка дорогая (сложные расчеты, SVG, анимации).
2. Что кеширует useMemo?
useMemo хук, который запоминает результат функции до тех пор, пока не изменятся зависимости. Он не предотвращает рендер, а лишь экономит вычисления внутри рендера.
const filtered = React.useMemo(() => {
return items.filter((item) => item.category === filter);
}, [items, filter]);
| Плюс | Минус |
|---|---|
| Кеширует тяжелые вычисления (фильтрация, сортировка) | Не спасет от лишних рендеров, если компонент все равно будет перерисован |
| Позволяет контролировать зависимости явно | Может стать “запаской”, если вычисления легкие лишний код |
Когда использовать:
- Вычисление требует значительных ресурсов (поиск, трансформы, большие массивы).
- Вычисление зависит от нескольких значений, меняющихся реже, чем каждый рендер.
3. Сценарий: таблица с фильтрами и пагинацией
Представим компонент DataTable. Он получает массив rows, текущий filter и page. Без мемоизации каждый ввод в поле фильтра приводит к полной пере-вычислению и перерисовке всех строк.
function DataTable({ rows, filter, page }) {
const filteredRows = React.useMemo(() => {
return rows.filter((r) => r.name.includes(filter));
}, [rows, filter]);
const pageRows = React.useMemo(() => {
const start = page * 10;
return filteredRows.slice(start, start + 10);
}, [filteredRows, page]);
return (
<div>
{pageRows.map((row) => (
<Row key={row.id} item={row} />
))}
</div>
);
}
Здесь useMemo отвечает за тяжелые операции (фильтрация, пагинация). Но строки-компоненты все равно могут перерисовываться каждый раз, если их props меняются.
4. Комбинируем React.memo и useMemo
Самый эффективный паттерн использовать оба инструмента: useMemo для расчетов, React.memo для “дешевых” строк.
const Row = React.memo(({ item }) => {
console.log("render row", item.id);
return <div>{item.title}</div>;
});
function DataTable({ rows, filter, page }) {
const filteredRows = React.useMemo(
() => rows.filter((r) => r.name.includes(filter)),
[rows, filter]
);
const pageRows = React.useMemo(() => {
const start = page * 10;
return filteredRows.slice(start, start + 10);
}, [filteredRows, page]);
return (
<div>
{pageRows.map((row) => (
<Row key={row.id} item={row} />
))}
</div>
);
}
Таблица теперь делает минимум работы: фильтрация один раз, а отдельные строки только если действительно изменились их данные.
5. Частые ошибки и антипаттерны
- Мемоизировать все подряд.
React.memoбез необходимости приводит к “прокисанию” кода, аuseMemoдля простых вычислений создает нагрузку на сравнение зависимостей. - Неправильные зависимости. Пропуск нужной зависимости в массиве
useMemoприводит к устаревшим результатам. - Сравнение по ссылке. Если вы передаете массив/объект, который каждый рендер создается заново,
React.memoбудет считать их разными и рендер будет происходить. - Забыть про
useCallback. Иногда вместо мемоизации функции лучше использоватьuseCallback, чтобы стабилизировать ссылки, передаваемые в дочерние компоненты. - Перепутать цель.
React.memoпро рендер,useMemoпро вычисления. Понимание этого различия спасает от большинства путаниц.
6. Чеклист перед продакшеном
- Выделил тяжелые расчеты в
useMemo? Проверил, что зависимости покрывают все входные данные. - Обернул часто-перерисовываемые дочерние компоненты в
React.memo? Убедился, что ихpropsстабильно сравниваются (примитивы, memo-объекты). - Протестировал профайлером React DevTools: есть ли реальное снижение времени рендера? Подробнее о профилировании смотрите React Profiler: практический гайд.
- Не забываю про
useCallbackдля функций-обработчиков, передаваемых в мемоизированные компоненты. - Удалил лишнюю мемоизацию, если измерения показывают, что она не дает выгоды.
Когда ваш интерфейс начинает “тормозить” от постоянных перерисовок, а данные тяжело фильтровать каждый кадр, комбинирование React.memo и useMemo становится простым и надежным решением.
Если списки становятся очень большими, рассмотрите виртуализацию списков.
Это именно тот набор инструментов, который позволяет держать рендер быстрым без излишней сложности кода.