Вы замечали, что интерфейс начинает "тормозить" при росте сложности компонентов? В большинстве случаев виноваты лишние ререндеры — когда компоненты пересчитываются без фактических изменений в данных. Это не просто микрооптимизация: в больших приложениях подобные потери могут складываться в сотни миллисекунд. Разберем практические методы решения проблемы.
Почему ререндеры вообще происходят?
React по умолчанию рендерит компонент при:
- Изменении его state через
useState
/useReducer
. - Изменении пропсов.
- Обновлении контекста, который использует компонент.
- Ререндере родительского компонента.
Последний пункт чаще всего становится источником проблем. Рассмотрим классический пример:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<Child /> {/* Перерендеривается при каждом клике! */}
</>
);
}
function Child() {
console.log("Child rendered!");
return <div>Static content</div>;
}
Здесь Child
не зависит от состояния count
, но ререндерится вместе с родителем. На первый взгляд — мелочь, но если таких компонентов десятки или они тяжелые, производительность деградирует.
Контролируем ререндеры с помощью мемоизации
React.memo для поверхностного сравнения пропсов
Оберните компонент в React.memo
, чтобы предотвратить ререндер, если пропсы не изменились:
const Child = React.memo(function Child() {
console.log("Child rendered!");
return <div>Static content</div>;
});
Теперь Child
не будет ререндериться при кликах в Parent
. Но есть нюанс: если передавать в Child
пропсы, особенно объекты или функции, это сработает, только если эти пропсы тоже стабильны.
useCallback для стабильности функций
Классическая ошибка: инлайновая функция в пропсе.
function Parent() {
const [count, setCount] = useState(0);
const handleAction = () => console.log("Action");
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<Child onAction={handleAction} />
</>
);
}
При каждом ререндере Parent
создается новая функция handleAction
. Для React.memo
это разные объекты — ререндер неизбежен. Используйте useCallback
:
const handleAction = useCallback(() => console.log("Action"), []);
Функция сохраняет идентичность между рендерами до изменения зависимостей.
useMemo для "тяжелых" вычислений и пропсов-объектов
Если компонент принимает объект или массив, инлайновая инициализация гарантированно создает новые ссылки. Решение:
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme: "dark" }), []);
return <Child config={config} /> ;
}
Тот же подход работает для вычислительно сложных операций:
const sortedList = useMemo(
() => hugeArray.sort(complexSortLogic),
[hugeArray] // Пересчет только при изменении массива
);
Работа с контекстом без ненужных обновлений
Контекст — частый провокатор лишних ререндеров. При обновлении значения контекста перерисовываются все потребители, даже если они используют только часть данных. Используйте селекторы:
// Плохо: компонент будет ререндериться при любом изменении контекста
const { user } = useContext(AppContext);
// Лучше: используем библиотеку с селектором
import { useAtom } from "jotai";
const user = useAtom(userAtom); // Перерисовка только при изменении userAtom
Если не хотите подключать внешние библиотеки, сделайте контекст более гранулярным:
// Мелкие контексты для конкретных данных
const UserContext = createContext();
const ThemeContext = createContext();
function App() {
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Child />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Теперь компонент, читающий только ThemeContext
, не будет реагировать на изменения в UserContext
.
Что не стоит мемоизировать без необходимости?
Избыточная мемоизация создает обратный эффект: расход памяти на хранение старых значений и затройки для их сравнения. Мемоизации не требуют:
- Примитивные пропсы (string, number, boolean), если они не меняются.
- Компоненты с интенсивной внутренней логикой, но редкими ререндерами родителя.
- Компоненты "листья" в дереве — их ререндер незначительно влияет на производительность.
Инструменты для диагностики
- React DevTools Profiler: записывайте сессии взаимодействий и находите лишние ререндеры.
- Highlight updates: включите подсветку ререндеров в React DevTools (настройки → "Highlight updates"). Компоненты будут мигать при обновлении.
- Консольные логи: самый простой способ для точечной проверки ("Компонент X перерендерился").
Не гонитесь за "идеальной" оптимизацией с самого начала. Сначала сделайте работающей логику, затем замерьте производительность и оптимизируйте узкие места. Избыточная мемоизация усложнит сопровождение кода без реального выигрыша. Помните: предсказуемое поведение и человекочитаемый код всегда приоритетнее микрооптимизаций.
Итоговая стратегия:
React.memo
для "тяжелых" компонентов с частыми проп-изменениями.- Кэшируйте функции через
useCallback
при передаче в мемоизированные компоненты. - Используйте
useMemo
для дорогих вычислений и сложных структур данных. - Делите контексты и используйте атомарные состояния.
- Профилируйте, прежде чем оптимизировать.
Корректное управление ререндерами сокращает время отклика интерфейса на 10-40%. В мире, где задержка в 100 мс уже заметна пользователю, это того стоит.