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
становится простым и надежным решением.
Если списки становятся очень большими, рассмотрите виртуализацию списков.
Это именно тот набор инструментов, который позволяет держать рендер быстрым без излишней сложности кода.