Столкнулись с подтормаживающими интерфейсами в React? Частая причина — избыточные ререндеры компонентов, лучше диагностируем проблему и учимся её решать.
Природа ререндеров в React
React перерисовывает компонент в трёх случаях:
- Изменение пропсов
- Изменение состояния (через
useState
,useReducer
) - Перерисовка родительского компонента
Коварный нюанс: Когда родительский компонент ререндерится — все дочерние компоненты перерисовываются по умолчанию, даже если их пропсы не изменились.
// Родительский компонент
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<Child /> {/* Рендерится при каждом клике! */}
</div>
);
};
const Child = () => {
console.log("Child rendered!"); // Логируется на каждый клик
return <div>Static Content</div>;
};
Почему это проблема? Для сложных компонентов избыточные ререндеры вызывают лаги в интерфейсе. В реальных приложениях такие случаи накапливаются лавинообразно.
Инструменты диагностики
1. DevTools Profiler
Записывайте и анализируйте рендеры компонентов. Смотрите на длительность коммитов и подсвеченные компоненты. Если компонент перерисовывается без изменения пропсов — это кандидат на оптимизацию.
2. Почему этот компонент ререндерится?
Установите React Developer Tools и пользуйтесь вкладкой ⚛️ Profiler. При выборе компонента он покажет:
- Что вызвало ререндер (пропсы, состояние, контекст)
- Покажет «глубину рендера» компонента
Техники оптимизации: от простого к сложному
1. React.memo
для мемоизации
Оберните «тяжёлые» дочерние компоненты в React.memo
, чтобы избежать ререндера при неизменных пропсах.
const ExpensiveComponent = ({ data }) => {
// Тяжёлые вычисления
return <div>{data}</div>;
};
export default React.memo(ExpensiveComponent); // Рендер только при изменении data
Важно: React.memo
не панацея. Он полезен только при частых ререндерах с одинаковыми пропсами. Для компонентов, постоянно получающих новые пропсы, он бесполезен.
Кастомная проверка пропсов
Когда пропсы — объекты или массивы:
React.memo(ExpensiveComponent, (prevProps, nextProps) => {
return prevProps.items.length === nextProps.items.length
&& prevProps.config.mode === nextProps.config.mode;
});
2. Управление функциями: useCallback
Функции, создаваемые внутри компонента, изменяются при каждом рендере. Это ломает memo
.
const Parent = () => {
const [count, setCount] = useState(0);
// ⛔ Плохо: новая функция при каждом рендере
const handleAction = () => { ... };
// ✅ Хорошо: мемоизация колбэка
const handleAction = useCallback(() => { ... }, []);
return <Child onAction={handleAction} />;
};
const Child = React.memo(({ onAction }) => { ... });
3. useMemo
для тяжёлых вычислений
Не пересчитывайте данные на каждом рендере, если зависимости не изменились.
const heavyComputation = (items) => {
// Сортировка, фильтрация, сложная логика...
return processedData;
};
const Component = ({ items }) => {
// ⛔ Плохо: пересчёт на каждый рендер
// const data = heavyComputation(items);
// ✅ Хорошо: мемоизация результата
const data = useMemo(() => heavyComputation(items), [items]);
return <Chart data={data} />;
};
Обязательное правило: при передаче функций в useMemo
избегайте сторонних эффектов (API-запросы, мутации).
Контекст: скрытая угроза
Изменение значения в Context.Provider
вызывает ререндер всех компонентов, использующих этот контекст — даже если они используют лишь неизменившуюся часть данных.
Решение: Разделяйте контексты.
// ⛔ До: один контекст на всё
<AppContext.Provider value={{ user, theme, cart }}>
<Header />
<Content />
</AppContext.Provider>
// ✅ После: разделение по смыслу
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<CartContext.Provider value={cart}>
<Header />
<Content />
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
Экстремальный случай: Мемоизация значений контекста.
const CartContextProvider = ({ children }) => {
const [cart, setCart] = useState([]);
// Мемоизируем объект контекста
const contextValue = useMemo(() => ({ cart, setCart }), [cart]);
return (
<CartContext.Provider value={contextValue}>
{children}
</CartContext.Provider>
);
};
Композиция против Prop Drilling
Чем меньше пропсов проходит через компонент — тем ниже риск лишних ререндеров. Иногда достаточно перестроить архитектуру.
До: Передача колбэков через слои
<Parent>
<Child>
<Grandchild onAction={handleAction} /> // Рендерится при обновлении Parent
</Child>
</Parent>
После: Использование Composition
const Parent = () => {
return (
<Child>
{/* Grandchild контент передаётся как чистое дерево */}
<Grandchild />
</Child>
);
};
const Child = ({ children }) => {
// children ререндерится ТОЛЬКО если изменились их пропсы
return <div>{children}</div>;
};
Когда не оптимизировать
- Профиль производительности чист. Не усложняйте код без замеров профилировщика.
- Слишком много мемоизации.
useMemo
иuseCallback
увеличивают объём памяти. Для лёгких компонентов оверхед может быть хуже пользы. - Компоненты уровня листьев. Ререндер кнопки или текста практически незаметен.
Реальный кейс: Оптимизация таблицы с фильтрами
Ситуация: Таблица из 100+ строк тормозит при вводе в фильтр.
Диагностика:
- Инпут фильтра живет в родительском компоненте
- При каждом вводе ререндерятся: родитель → заголовок таблицы → все строки
Решение:
- Выносим инпут фильтра в отдельный компонент (он не должен влиять на таблицу).
- Таблицу оборачиваем в
React.memo
. - Каждую строку мемоизируем по ID.
const TableRow = React.memo(({ row }) => { ... });
const Table = ({ data }) => {
return data.map(row =>
<TableRow key={row.id} row={row} />
);
};
export default React.memo(Table);
- Фильтры храним через
useDeferredValue
(React 18+), чтобы не блокировать интерфейс:
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter); // "отложенное" значение
useEffect(() => {
// Фильтрация будет происходить при "простое"
}, [deferredFilter]);
Код-ревью чеклист
Проверяйте код на:
- Неумемоизированные колбэки, передаваемые в
memo
-компоненты - Вложенные объекты/массивы в пропсах, изменяющиеся при каждом рендере
- Контексты, объединяющие независимые данные
- Ререндеры при прокидке
children
Что запомнить
- Измеряйте перед оптимизацией. Без профайлера вы стреляете наугад.
React.memo
,useCallback
,useMemo
— ваши лучшие инструменты для контроля ререндеров.- Дробите большие контексты.
- Композиция компонентов снижает сцепление.
- Сложные вычисления делегируем в воркеры или
useMemo
.
Оптимизация рендеров — это баланс между производительностью и сложностью кода. Инициируйте изменения там, где пользователь ощущает разницу. Иногда достаточно поправить одно ключевое место вместо применения мемоизации повсюду.
Теперь, когда вкладка Performance показывает зелёные цифры — можно и окунуться в чашку кофе.