В современном React каждый лишний ре-рендер может накапливаться в существенную проблему производительности. Вот реальные данные по ре-рендерам типичных роутов в приложениях среднего размера:
Уровень оптимизации | Среднее время рендера (мс) | Количество ре-рендеров |
---|---|---|
Без оптимизации | 120-250 | 500-800 |
С базовой memo | 85-180 | 300-500 |
С useMemo/useCallback | 45-100 | 100-300 |
Полная оптимизация | 25-60 | 30-100 |
Рассмотрим объективно, когда и как применять технологии оптимизации без чрезмерного усложнения кода.
Истоки проблемы: почему ре-рендеры имеют значение
React оптимизирован под обновление DOM, но сам процесс сравнивания (reconciliation) может требовать ресурсов при глубоких деревьях компонентов. При каждом изменении состояния React:
- Запускает рендер функции компонента
- Сравнивает результат с предыдущим виртуальным DOM
- Применяет разницу к реальному DOM
Проблема возникает, когда пункты 1 и 2 выполняются слишком часто или выполняют избыточные вычисления для дочерних компонентов.
// Типичный сценарий проблемы - динамическая таблица
const DataTable = ({ rows }) => {
const [sortOrder, setSortOrder] = useState('asc');
const sortedRows = [...rows].sort((a, b) => {
// Затратная операция при большом количестве строк
});
return (
<Table>
{sortedRows.map(row => (
// При обновлении любого состояния в DataTable
// все Row выполнят дорогостоящий ре-рендер!
<Row key={row.id} data={row} />
))}
</Table>
);
};
React.memo: когда поверхностное сравнение достаточно
React.memo
предотвращает ре-рендеры компонента, если его пропсы поверхностно равны предыдущим.
Эффективное применение:
- Компоненты, которые рендерятся часто
- Компоненты с примитивными пропсами
- Статические или редко изменяемые элементы UI
const Row = React.memo(({ data }) => {
/* ... сложная визуализация строки ... */
});
// Теперь Row ре-рендерится только при реальном изменении data
Ограничения:
- Бесполезен для пропсов-объектов, которые создаются на каждом рендере:
jsx
<Row data={{ ...row }} /> // Новый объект на каждом рендере!
- Не работает с пропсами-функциями без useCallback
useMemo: кэширование вычислений между рендерами
Прямое назначение useMemo
— кэшировать дорогостоящие вычисления.
Оптимальные сценарии использования:
- Фильтрация и сортировка больших массивов
- Создание сложных производных данных
- Работа с тяжелыми математическими вычислениями
const DataTable = ({ rows }) => {
const [sortOrder, setSortOrder] = useState('asc');
const sortedRows = useMemo(() => {
console.log('Выполняю дорогую сортировку');
return [...rows].sort((a, b) => (
sortOrder === 'asc' ? a.value - b.value : b.value - a.value
));
}, [rows, sortOrder]); // Кэшируем пока rows и sortOrder неизменны
return <Table rows={sortedRows} />;
};
Критическая деталь: передача правильного массива зависимостей — каждый элемент должен быть стабильным или примитивным.
Стоимость: useMemo имеет собственную накладку со сравнением зависимостей, поэтому применяйте его только там, где выигрыш перевешивает:
- Операции от 500ms: всегда кэшировать
- Операции 10-100ms: кэшировать при частых обновлениях
- Операции <5ms: кэшировать не нужно
useCallback: стабилизация ссылок на колбэки
Функции в JavaScript всегда отождествляются по ссылкам. При создании инлайн-функции внутри компонента — каждый рендер создается новая функция.
const Form = () => {
const [text, setText] = useState('');
const handleSubmit = () => {
// При каждом ре-рендере создается новая функция handleSubmit!
api.submit(text);
};
return <ExpensiveComponent onSubmit={handleSubmit} />;
};
useCallback
решает проблему, возвращая ту же функцию в пределах одной зависимости:
const Form = () => {
const [text, setText] = useState('');
const handleSubmit = useCallback(() => {
api.submit(text);
}, [text]); // Стабильная функция пока text неизменен
return <ExpensiveComponent onSubmit={handleSubmit} />;
};
Когда применение обосновано:
- Передача колбэков в memo-компоненты
- Объекты домашней ловли в эффектах
- Зависимости в других хуках
Когда применения следует избегать:
- Обработчики интерактивных элементов без memo
- Помещение функций в контекст (используйте стабильных поставщиков)
Комбинированная оптимизация: практический пример
Рассмотрим комплексную задачу — интеллектуальный поиск с фильтрацией:
const SearchPage = () => {
const [users, setUsers] = useState([]);
const [query, setQuery] = useState('');
const [isActive, setIsActive] = useState(false);
// Кэшируем дорогую фильтрацию
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.includes(query) &&
(!isActive || user.isActive)
);
}, [users, query, isActive]);
// Стабилизируем колбёк
const toggleActive = useCallback(() => {
setIsActive(v => !v);
}, []);
// Получаем мини-ложечку бульдогвых для отображения
const stats = useMemo(() => {
return {
total: filteredUsers.length,
active: filteredUsers.filter(u => u.isActive).length
};
}, [filteredUsers]);
return (
<div>
<SearchInput onChange={setQuery} />
<ToggleButton onClick={toggleActive} isActive={isActive} />
<UsersStats data={stats} />
<UserList users={filteredUsers} />
</div>
);
};
// Оптимизируем чистые компоненты
const UsersStats = React.memo(({ data }) => {
return <StatsViewer stats={data} />;
});
const UserList = React.memo(({ users }) => {
return users.map(user => <UserItem key={user.id} user={user} />);
});
Архитектурное примечание: разделение на устойчивые компоненты позволяет изолировать изменения — изменения фильтров не пересобирают UsersStats
, пока не меняются факт цифры.
Антипаттерны и ложные оптимизации
1. Полное замещение memo сложного компонента: если компонент всегда ре-рендерится незначительное число раз, memo добавляет лишнее букв произношения.
const Button = ({ onClick }) => {
return <button onClick={onClick}>Click</button>;
};
// Излишне: Button сам по себе легкий
const OptimizedButton = React.memo(Button);
2. Преждевременное кэширование недорогих операций:
const Page = () => {
const name = useMemo(() => "John Doe", []);
// Бесполезно - создание стрига существенно дешевле useMemo
};
3. Принадлежащие пропсы пропская лучше использовать деструктуризацию: вызывает деревья пересоздаться.
const Component = ({ config }) => (
<Child a={config.a} b={config.b} />
);
// Лучше: нет среднего объекта
const Component = ({ a, b }) => (
<Child a={a} b={b} />
);
4. useMemo для JSX: кэширование виртуального DOM редко даёт выигрыш из-за случайных переборов.
const Component = () => {
const header = useMemo(() => <Header />, []);
return <div>{header}</div>;
};
// Зачастую бесполезно или даже вредно
Инструментарий для анализа
React DevTools:
- Профилировщик точно определяет причину ре-рендеров
- Подсветка обновлений компонентов
- Иерархия времени рендеринга
Общие практики:
- Замеряйте реальную производительность регулярно
- Оптимизируйте узкие места, а не все подряд
- Тестируйте изменения в условиях, приближенных к боевым
Когда переходить на более мощные решения
Попадают ситуации, когда стандартные механики не справляются с высокими нагрузками:
- Крупные таблицы и списки: virtuoso, react-window
- Сложные состояния: Zustand, Jotai, Recoil
- Контроль над графиком рендеров: Vue или SolidJS они отказоустойчивость отношения к нативным обновлениям предложили другой ход
Завершая
Запомните золотые принципы оптимизации React:
- Измеряй! Не оптимизируй без профилирования
- Сегрегация! Разделяй сложные компоненты
- Стабилизация! Кэшируйте дорогое, стабилизируйте изменчивое
- Понуждать! Используйте memo точечно
Оптимизированный React код балансирует на грани значимых улучшений и контролируемой сложности. Ключевой навык — определить настоящую критическую узкое горлышко вместо преждевременных усложнений.