Рендеринг в React работает как хорошо отлаженный механизм — до тех пор, пока случайно не превратится в ад лишних перерисовок. Разработчики часто замечают «тормоза» в интерфейсе, не понимая, что сами создали ловушку: компонент рендерится чаще, чем необходимо, иногда десятки раз в секунду. Но как отличить нормальный процесс обновления от проблемного? И главное — что с этим делать?
Почему компоненты ререндерятся слишком часто
Рассмотрим пример компонента списка покупок, где приложение тормозит при вводе текста:
function ShoppingList() {
const [items, setItems] = useState(initialItems);
const [search, setSearch] = useState('');
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ItemList items={filteredItems} />
</>
);
}
Здесь проблема в строке items.filter(...)
. При каждом нажатии клавиши:
- Выполняется дорогостоящая фильтрация массива
- Создается новый массив
filteredItems
<ItemList>
получает новую ссылку в пропсеitems
и перерисовывается
Но хуже другое: если items
не изменились, а search
пуст — фильтрация все равно происходит, а новый массив (даже идентичный по содержанию) заставляет дочерние компоненты обновляться.
Инструменты диагностики
Прежде чем оптимизировать, нужно убедиться в наличии проблемы. Откройте React DevTools:
- Включите подсветку обновлений в настройках
- Проанализируйте частоту рендеров компонента
- Используйте проп
highlightUpdates={false}
для<ItemList>
при передаче через React.memo
Для сложных случаев подключите why-did-you-render
:
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, { trackAllPureComponents: true });
Контроль вычислений с useMemo
Модифицируем пример с фильтрацией:
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}, [items, search]); // Пересчитывать только при изменении items или search
Теперь:
- Фильтрация выполняется только при изменении исходных данных или поискового запроса
<ItemList>
получает стабильную ссылку, если входные данные не менялись- Для 10 000 элементов экономия может составить 200-300 мс на рендер
Но useMemo — не панацея. Для небольших массивов (до 100 элементов) мемоизация может быть избыточна — накладные расходы на сравнение зависимостей превысят выгоду.
useCallback для функций: когда это имеет смысл
Допустим, у нас есть компонент кнопки:
const ExpensiveButton = React.memo(({ onClick }) => {
// Тяжелые вычисления при рендере
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const handleClick = () => console.log('Clicked');
return <ExpensiveButton onClick={handleClick} />;
}
Здесь handleClick
создается заново при каждом рендере Parent, что заставляет ExpensiveButton перерисовываться. Используем useCallback:
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Стабильная ссылка между рендерами
Но есть нюансы:
- Для пустого массива зависимостей убедитесь, что функция действительно не зависит от состояния
- Если функция использует свежие значения из замыкания, укажите зависимости явно:
const handleClick = useCallback(() => {
dispatch(actionCreator(value)),
}, [value]); // Функция обновится при изменении value
Альтернативные подходы
- Разделение компонентов:
function SearchInput({ onChange }) {
const [value, setValue] = useState('');
// Обработка ввода локализована в одном месте
}
function ShoppingList() {
// Основная логика остается здесь
}
- Примитивы вместо объектов:
// Было
<UserCard user={user} />
// Стало (если нужны только имя и аватар)
<UserCard name={user.name} avatar={user.avatar} />
- Ключи для списков: Неоптимально:
{items.map((item, index) => (
<Item key={index} {...item} />
))}
Лучше:
{items.map(item => (
<Item key={item.id} {...item} />
))}
Когда оптимизация избыточна
- Для компонентов с простым render (нет тяжелых вычислений)
- В случаях, где ререндеры происходят редко (настройки профиля раз в месяц)
- Когда приложение и так работает плавно (60 FPS)
Практические рекомендации
- Начинайте разработку без оптимизаций
- Используйте React.memo для сложных компонентов (списки, графы, таблицы)
- Включайте memoization только при явных признаках проблем
- Тестируйте на реальных устройствах (слабые Android-телефоны покажут проблемы раньше M1 MacBook)
- Измеряйте производительность до и после с помощью:
console.time('Filter operation');
// Операция...
console.timeEnd('Filter operation');
Не гонитесь за микрооптимизациями. Часто достаточно:
- Грамотной структуры компонентов
- Правильной работы с ключами в списках
- Предотвращения «ступенчатых» обновлений цепочки компонентов
Сложные приложения требуют баланса между производительностью и поддерживаемостью кода. Иногда лучше допустить лишний ререндер, чем сделать код непонятным из-за чрезмерной мемоизации. Оптимизируйте осознанно, с пониманием того, как работает React под капотом.