React.memo и useMemo
React.memoпредотвращает ре-рендер компонента, если его props не изменились;useMemoиuseCallbackмемоизируют вычисления и функции внутри компонента. Все три инструмента снижают лишние ре-рендеры и дорогостоящие вычисления.
Зачем нужно
По умолчанию React ре-рендерит компонент каждый раз, когда ре-рендерится родитель. React.memo прерывает эту цепочку. Без useMemo тяжёлые вычисления (фильтрация 10k элементов) выполняются заново на каждый ре-рендер. Правильное применение улучшает INP.
Где используется
- Компоненты в длинных списках (ProductCard, TableRow)
- Тяжёлые вычисления (фильтрация, сортировка больших массивов)
- Обработчики событий, передаваемые дочерним компонентам с React.memo
- Контекст: стабилизация значения Provider для предотвращения массового ре-рендера
Основной контент
React.memo — мемоизация компонента
// БЕЗ memo: перерендерится при каждом ре-рендере родителя
function ProductCard({ product }) {
return <div>{product.name} — {product.price}₽</div>;
}
// С memo: перерендерится только если product изменился
const ProductCard = React.memo(function ProductCard({ product }) {
return <div>{product.name} — {product.price}₽</div>;
});
// Кастомный comparator (когда нужно только часть props)
const ProductCard = React.memo(
function ProductCard({ product, onAddToCart }) { /* ... */ },
(prevProps, nextProps) =>
prevProps.product.id === nextProps.product.id &&
prevProps.product.price === nextProps.product.price
);
useMemo — мемоизация значения
function ProductList({ products, searchQuery, sortBy }) {
// БЕЗ useMemo: фильтрация выполняется при КАЖДОМ ре-рендере
const filtered = products
.filter(p => p.name.includes(searchQuery))
.sort(/* ... */);
// С useMemo: пересчёт только при изменении products или searchQuery
const filtered = useMemo(
=>
products
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1),
[products, searchQuery, sortBy] // dependencies
);
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}
useCallback — мемоизация функции
// Проблема: при каждом ре-рендере создаётся новая функция
// ProductCard с React.memo ре-рендерится, потому что onAddToCart !== onAddToCart
function Cart() {
const [items, setItems] = useState();
// БЕЗ useCallback: новая функция при каждом рендере
const handleAddToCart = (product) => {
setItems(prev => [...prev, product]);
};
// С useCallback: стабильная ссылка
const handleAddToCart = useCallback((product) => {
setItems(prev => [...prev, product]);
}, ); // Пустые deps — функция не меняется
return <ProductCard product={p} onAddToCart={handleAddToCart} />;
}
Когда НЕ нужно мемоизировать
// НЕ нужно memo/useMemo:
// 1. Простые компоненты без тяжёлой логики
// 2. Компоненты, которые и так всегда перерендерятся (изменяются props)
// 3. Вычисления, которые занимают <1ms
// 4. Примитивные значения (string, number) — ссылочное равенство не проблема
// Memo добавляет overhead: хранение previous props + сравнение
// Используй только когда Profiler показывает реальную проблему
React Profiler — измерение перед оптимизацией
import { Profiler } from 'react';
function onRender(id, phase, actualDuration, baseDuration) {
if (actualDuration > 16) { // Медленнее 60fps
console.warn(`Slow render: ${id} took ${actualDuration}ms`);
}
}
<Profiler id="ProductList" onRender={onRender}>
<ProductList products={products} />
</Profiler>
Частые ошибки
- Мемоизация всего подряд без профилирования — overhead превышает пользу
useCallbackбез соответствующегоReact.memoна дочернем компоненте — бесполезно- Нестабильные зависимости в
useMemo(объекты/массивы) — мемоизация не работает - Пропущенные зависимости в deps array — устаревшие значения (stale closure)
Связанные темы
- _MOC Производительность
- React -- виртуальный DOM и reconciliation
- Виртуализация длинных списков
- React -- Suspense и lazy