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)

Связанные темы

Ресурсы