Мемоизация в React: ускоряем рендер без лишних расходов
Практические рекомендации по использованию useMemo, useCallback и React.memo для снижения количества перезапусков и экономии CPU в React-приложениях.
Если список “залипает”, а консоль полна сообщений о частых рендерах, вероятно, вы повторно вычисляете результаты, которые можно кешировать. Мемоизация в React способ сохранять результаты функций между рендерами, чтобы избежать лишней работы.
Новички часто добавляют React.memo
к каждому компоненту, полагая, что это решит все проблемы. На деле такая “массовая” оптимизация может ухудшить производительность, потому что сравнение пропсов также требует ресурсов. Понимание, где действительно нужен кеш, экономит и код, и время отладки.
В примере разбирается задача: большой список карточек, каждая получает данные через тяжелую функцию преобразования. Показан путь от простого useMemo
к продуманному использованию useCallback
и React.memo
.
1. Что такое мемоизация и зачем она в React?
Мемоизация это кэширование результата функции при тех же входных аргументах. В React она позволяет избежать повторных вычислений во время re-render.
function expensiveCalc(items) {
console.log("calc");
return items.reduce((a, b) => a + b, 0);
}
// При каждом рендере будет "calc"
const total = expensiveCalc(props.numbers);
Состояние | Вычисления | Рендеров | CPU-затраты |
---|---|---|---|
без кеша | каждый раз | каждый | высокие |
с useMemo | один раз | каждый | низкие |
с React.memo | при изменении props | средние |
useMemo
хранит значение между рендерами и обновляет его только при изменении зависимостей. React.memo
делает то же самое, но на уровне компонента, сравнивая пропсы.
2. Когда использовать useMemo
?
useMemo
уместен, когда внутри компонента есть тяжелая логика, зависящая от пропсов или состояния.
function ProductList({ products, filter }) {
const filtered = useMemo(() => {
return products.filter((p) => p.category === filter);
}, [products, filter]);
return (
<ul>
{filtered.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Чек-лист для useMemo
- Функция действительно тяжелая и ее можно вычислять реже.
- Зависимости перечислены явно.
- Возвращаемое значение не создается заново каждый рендер (иначе кеш не поможет).
Если зависимости часто меняются, эффект от useMemo
снижается тогда ищем альтернативу.
3. Как правильно применять React.memo
к компонентам?
React.memo
оборачивает функциональный компонент и делает поверхностное сравнение пропсов.
const Card = React.memo(function Card({ title, onClick }) {
console.log("render Card");
return (
<div onClick={onClick}>
<h3>{title}</h3>
</div>
);
});
Плюсы | Минусы |
---|---|
Сокращает лишние рендеры | Сравнение поверхностное; может не заметить глубокие изменения |
Простой синтаксис | Добавляет небольшие накладные расходы при сборке |
Важно: если onClick
передается как анонимная функция, каждый рендер создает новый референс, и React.memo
будет считать пропсы измененными. В этом случае стабилизировать колбэк помогает useCallback
.
4. useCallback
как вспомогательный инструмент
useCallback
возвращает мемоизированную версию функции и работает аналогично useMemo
, но для функций.
function List({ items }) {
const handleClick = useCallback((id) => {
console.log("clicked", id);
}, []);
return items.map((item) => (
<Card
key={item.id}
title={item.title}
onClick={() => handleClick(item.id)}
/>
));
}
Здесь handleClick
стабилен, но анонимная стрелка внутри onClick
все равно создает новый пропс. Можно избавиться от нее, передавая уже готовый колбэк:
const handleClick = useCallback(id => () => console.log('clicked', id), []);
...
<Card onClick={handleClick(item.id)} />
Типичные ошибки
- Оставлять анонимные функции в JSX приводит к лишним рендерам.
- Игнорировать массив зависимостей мемоизация ломается.
- Оборачивать каждый компонент в
React.memo
без профилирования может замедлить сборку.
5. Профилирование: где действительно нужна оптимизация?
Перед тем как добавить useMemo
или React.memo
, измерьте влияние. В Chrome DevTools → Performance:
- Запустите профиль без оптимизаций.
- Добавьте кеширование к подозрительной функции.
- Сравните графики “Self Time” и “Total Time”.
Если разница менее 5 % CPU, кешировать смысл нет код усложнится без выгоды.
Показатель | Действие |
---|---|
Self Time > 10 ms | Добавить useMemo /React.memo |
Частые ререндеры > 3 | Проверить зависимости |
Стабильные пропсы | Оставить без memo |
6. Практический пример: виртуализированный список
Сценарий: список из 10 000 карточек, каждая вычисляет цену через сложный алгоритм. Решение сочетание useMemo
(для расчета цены) и React.memo
(для карточки).
function PriceCard({ product }) {
const price = useMemo(() => calculatePrice(product), [product]);
return (
<div className="card">
<h4>{product.name}</h4>
<p>{price}$</p>
</div>
);
}
const MemoPriceCard = React.memo(PriceCard);
function ProductGrid({ products }) {
return (
<VirtualList // из react-window
height={600}
itemCount={products.length}
itemSize={80}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<MemoPriceCard product={products[index]} />
</div>
)}
</VirtualList>
);
}
Каждый элемент пересчитывается только при изменении своего product
, а список рендерит лишь видимые элементы. При скролле нагрузка на CPU почти не меняется.
Чек-лист по оптимизации рендеров в React
- Выявите “дорогие” вычисления профилируйте, ищите функции с длительным временем. Используйте React Profiler для детального анализа.
- Применяйте
useMemo
только к значениям с ограниченным набором зависимостей. - Оборачивайте компоненты в
React.memo
лишь тогда, когда пропсы действительно стабилизированы. Подробнее в статье React.memo vs useMemo. - Стабилизируйте колбэки через
useCallback
, иначеmemo
будет срабатывать каждый рендер. - Проверяйте эффект без измерений оптимизация может стать лишним грузом.
Мемоизация полезный, но деликатный инструмент. Используйте ее, когда действительно экономите ресурсы, а не “по привычке”.