Отзывчивый UI — не просто улучшение UX, а требование современных веб-приложений. React автоматически управляет обновлениями интерфейса, но эта магия порой оборачивается неоправданно частыми ререндерами компонентов. Мы разберем инструменты оптимизации в экосистеме React, выходящие за рамки базового применения.
Проблема избыточных ререндеров
Рассмотрим компонент ProductList
, отображающий 1000 товаров:
const ProductList = ({ products, onSelect }) => (
<ul>
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
onClick={() => onSelect(product.id)}
/>
))}
</ul>
);
При обновлении любого свойства родителя или общего стейта весь список перерисовывается. Для небольших списков это некритично, но при масштабировании возникают лаги интерфейса. Проблема усугубляется при глубоком вложении компонентов.
Факторы триггеров ререндеров:
- Изменение пропсов (даже объект с идентичными полями)
- Обновление состояния (useState, useReducer)
- Обновление контекста (useContext)
Мемоизация компонентов с React.memo
React.memo
кэширует результат рендера, повторный ререндер происходит только при изменении пропсов (по shallow compare). Модифицируем ProductItem
:
const ProductItem = React.memo(({ product, onClick }) => {
// Тяжелые вычисления
const discountedPrice = calculateDiscount(product.price, 15);
return (
<li onClick={onClick}>
{product.name} - ${discountedPrice}
</li>
);
});
Теперь ProductItem
ререндерится только при изменении product
или onClick
. Но здесь кроется ловушка: () => onSelect(product.id)
при каждом рендере родителя создаёт новую функцию, что приводит к ререндерам всех элементов!
Фиксация колбэков через useCallback
Перепишем обработчик с использованием useCallback
:
const ProductList = ({ products, onSelect }) => {
const handleSelect = useCallback((id) => {
onSelect(id);
}, [onSelect]); // Зависимость обязательна
return (
<ul>
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
onClick={handleSelect}
/>
))}
</ul>
);
};
useCallback
мемоизирует ссылку на функцию. Все ProductItem
теперь получают стабильный onClick
, ререндер происходит только при реальном изменении product
.
useMemo: кэш для тяжёлых вычислений
Вернемся к calculateDiscount
внутри ProductItem
. Вычисление скидки запускается при каждом рендере, даже если цена не изменилась. Исправляем:
const ProductItem = React.memo(({ product, onClick }) => {
const discountedPrice = useMemo(() => {
return calculateDiscount(product.price, 15);
}, [product.price]); // Единственная зависимость
return <li onClick={() => onClick(product.id)}>{product.name} - ${discountedPrice}</li>;
});
useMemo кэширует значение. Вычисления происходят только при изменении product.price
.
Глубокое сравнение пропсов
Что если product
— сложный объект с вложенными полями? Shallow compare в React.memo
не заметит изменения во внутренних свойствах. Возможны два решения:
// Кастомная функция сравнения
React.memo(ProductItem, (prevProps, nextProps) => {
return prevProps.product.id === nextProps.product.id
&& prevProps.product.price === nextProps.product.price;
});
// Или нормализация данных: передача примитивов
<ProductItem
id={product.id}
price={product.price}
name={product.name}
/>
Когда оптимизация становится антипаттерном:
- Компоненты с частым изменением пропсов (кэш никогда не используется)
- Использование
useMemo/useCallback
для примитивов - Мемоизация JSX блоков без реальных вычислений
- Избыточная вложенность мемоизированных компонентов
Типичная ошибка с зависимостями
Неправильное применение зависимостей сводит оптимизацию на нет:
const totalPrice = useMemo(() => (
products.reduce((sum, p) => sum + p.price, 0)
), []); // ❌ Зависимости пусты — значение никогда не обновится
const handleSelect = useCallback((id) => (
setSelectedIds(prev => [...prev, id])
), []); // ✅ Корректно: функция не зависит от внешних значений
Профилирование производительности
Без данных все оптимизации слепы. Используйте:
- React DevTools Profiler (замеры времени рендера)
- Вкладка Performance в Chrome DevTools
- Why Did You Render для отслеживания избыточных ререндеров
- React.memo с выводом в консоль при ререндере
Практические рекомендации:
- Сначала пишите без оптимизаций, замеряйте профилировщиком
- Мемоизируйте компоненты с тяжёлым рендером (списки, графики)
- useCallback — для функций, передаваемых в редко обновляемые компоненты
- useMemo — для дорогих вычислений при каждом рендере
- Нормализуйте данные на границах компонентов
- Избегайте ререндеров контекста мемоизацией провайдера
Оптимизация — баланс между производительностью и сложностью кода. Бездумная мемоизация усложнит логику без заметных улучшений. Начинайте с горячих точек, выявленных объективными замерами. Современные API React предоставляют точные инструменты для контроля рендеринга — применяйте их точечно и осмысленно.