В крупных React-приложениях замедление рендеринга редко проявляется как единовременный провал производительности. Оно накапливается: падает интерактивность интерфейса, вкладка браузера начинает нагружать процессор, пользовательские действия ощущаются "вялыми". Эта статья разбирает распространённую причину таких проблем — избыточные вычисления и ререндеры — и даёт практические инструменты решения.
Проблема: Дорогостоящие вычисления при каждом рендере
Рассмотрим простой компонент списка с сортировкой:
function ProductList({ products, category }) {
const filteredProducts = products.filter(
p => p.category === category
);
const sortedProducts = [...filteredProducts].sort(
(a, b) => a.price - b.price
);
return (
<div>
{sortedProducts.map(product => (
<ProductItem
key={product.id}
product={product}
onSelect={() => handleSelect(product.id)}
/>
))}
</div>
);
}
Каждый рендер компонента выполняет:
- Фильтрацию массива продуктов через
filter()
- Создание нового массива через spread-оператор
- Сортировку массива
- Создание новых коллбэков
onSelect
для каждогоProductItem
При изменении любого родительского стейта — даже не связанного с products
или category
— весь этот процесс запускается снова. Для больших массивов (10 000+ элементов) или тяжёлых вычислений такой подход недопустим.
Разрушаем проблему методологично
Идентификация дорогостоящих операций
Первое правило оптимизации: не угадывать, а измерять. Для этого используются:
-
React DevTools Profiler
Фиксирует фактические временные затраты компонентов во время рендера. -
Простейший замер производительности:
console.time('filterAndSort');
// ... операции с массивом
console.timeEnd('filterAndSort');
- Пакет
why-did-you-render
для отслеживания неожиданных ререндеров
Решение 1: useMemo для кеширования вычислений
const sortedProducts = useMemo(() => {
const filtered = products.filter(p => p.category === category);
return [...filtered].sort((a, b) => a.price - b.price);
}, [products, category]);
Ключевые моменты:
- Вынесите все зависимые значения в зависимости (
products
,category
) - Кешируйте только стоимость операции > 1ms
- Избегайте ранней оптимизации: сначала измерьте
Решение 2: useCallback для стабильных функций
Убираем создание новой функции при каждом рендере:
const handleSelect = useCallback((productId) => {
// логика выбора
}, []);
// В ProductItem:
<ProductItem
onSelect={handleSelect}
productId={product.id}
/>
Важные нюансы:
-
Коллбэки с параметрами
Вместо встраивания аргументов в коллбэк (onSelect={() => handleSelect(id)}
) передавайтеid
как проп:jsx<ProductItem onSelect={handleSelect} productId={product.id} />
-
Передача стейта в коллбэки
Для актуальных значений используйте setState с функцией обновления:jsxconst handleSelect = useCallback(() => { setSelectedProduct(prev => prev.id === productId ? null : productId); }, [productId]);
Решение 3: React.memo для регулирования ререндеров
Оптимизируем напрямую дочерние компоненты:
const ProductItem = React.memo(({ product, onSelect }) => {
return (
<div onClick={onSelect}>
<h3>{product.name}</h3>
<p>{product.price}$</p>
</div>
);
});
Когда использовать:
- Компонент часто ререндерится с теми же пропсами
- Рендер компонента ощутимо затратен (> 3ms)
- Пропсы простые (примитивы, стабильные объекты)
Реальные ограничения:
React.memo(ProductItem, (prevProps, nextProps) => {
return prevProps.product.id === nextProps.product.id;
});
Кастомная функция сравнения заменяет поверхностное сравнение, но требует ручной реализации.
Архитектурные компромиссы
Когда использовать эти методы? Рассмотрим матрицу решений:
Показатель | useMemo | useCallback | React.memo |
---|---|---|---|
Пространство в памяти | ++ | ++ | + |
Задержка рендеринга | +++ | ++ | +++ |
Сложность кода | + | + | ++ |
Зависимость от стабильности пропсов | Высокая | Высокая | Критична |
Практические рекомендации:
- Начните с
useMemo
вместоuseState
для производных данных - Оберните в
useCallback
функции, передаваемые более чем на 2 уровня вниз - Тестируйте на реальных размерах данных для целевых устройств
Деструктурируем весь пример с оптимизациями
const ProductList = ({ products, category }) => {
const sortedProducts = useMemo(() => {
const filtered = products.filter(p => p.category === category);
return [...filtered].sort((a, b) => a.price - b.price);
}, [products, category]);
const handleSelect = useCallback((productId) => {
setSelectedId(prev => prev === productId ? null : productId);
}, []);
return (
<div>
{sortedProducts.map(product => (
<MemoizedProductItem
key={product.id}
productId={product.id}
name={product.name}
price={product.price}
onSelect={handleSelect}
/>
))}
</div>
);
};
const MemoizedProductItem = React.memo(({ productId, name, price, onSelect }) => (
<div onClick={() => onSelect(productId)}>
<h3>{name}</h3>
<p>{price}$</p>
</div>
));
Когда оптимизации имеют цену
Избегайте ловушек:
-
Объектная тождественность в зависимостях
jsx// 👎 Способствует созданию нового массива на рендер useMemo(() => ..., [JSON.stringify(products)]) // 👍 Нормализуйте структуру данных на уровне API/reselect
-
Избыточное мемоизирование
Замеряйте перед добавлением — затраты памяти должны окупаться -
Разбиение на компоненты
Иногда оптимальнее разбить компонент вместо мемоизации:jsx// Вместо мемоизации огромного списка <HugeList> {/* 5000 элементов */} </HugeList> // Оптимизировать только видимую область: <VirtualizedList> {/* 20 элементов в viewport */} </VirtualizedList>
Выводы
Оптимизации в React требуют системного подхода:
- Измерять реальные показатели перед оптимизацией
- Изолировать изменения через строгие правила зависимостей
- Проверять результат в производственных условиях
Производительность — это не про абстрактные бенчмарки. Это про гарантию реакции интерфейса на действия пользователя за 100мс, про плавность анимаций на стареньком смартфоне, про комфорт работы. Используйте useMemo
, useCallback
и React.memo
сознательно — как хирургические инструменты, а не швейцарские ножи.
Дополнительные инструменты:
- React DevTools Profiler Component Measures
<React.Profiler>
в production- Lighthouse Performance Audits
- Throttling CPU в Chrome Developer Tools