Як боротися із зайвими рендерами у React
React робить створення інтерактивних інтерфейсів простішим завдяки своїй декларативній природі та оновленню тільки тих частин UI, що змінилися. Проте іноді додатки можуть відчувати проблеми з продуктивністю, коли відбувається занадто багато рендерів (re-renders). У цій статті ми розглянемо, чому це трапляється, як помітити надлишкові рендери та які підходи можуть знизити їх кількість.
Що таке зайві рендери
Зайві (або повторні) рендери — це ситуації, коли компонент React перерисовується без реальної потреби. Наприклад, коли стан або пропси компонента не змінились, але компонент все одно викликає логіку рендеру (і потенційно рендерить усіх своїх нащадків). У невеликих додатках це може бути не дуже критично, проте у великих застосунках з великою кількістю компонентів — збільшує час відгуку та знижує продуктивність.
Причини зайвих рендерів
Неправильне використання контексту
Якщо контекст (React Context) містить багато даних, то кожна зміна в ньому тригерить повторний рендер усіх компонентів, які його споживають, навіть якщо їм не потрібна зміна.
Анонімні функції та об’єкти у пропсах
Якщо в пропсах передаються нові об’єкти (наприклад, {}
) чи анонімні стрілкові функції, React вважає, що пропс змінився, і рендер відбувається знову.
Використання нових посилань у стані
Якщо стан компонента зберігає об’єкти або масиви, і при кожному оновленні створюються нові копії (навіть якщо вони логічно не змінилися), це може спровокувати повторний рендер.
Неоптимальне використання memo, useMemo, useCallback
Якщо компоненти не мемоізуються або хибно мемоізуються, React може рендерити навіть тоді, коли дані не змінилися.
Як виявити зайві рендери
Консольне логування
Простий спосіб — поставити console.log('render component')
у тілі компонента чи використати React DevTools, щоб бачити, які компоненти повторно рендеряться.
React DevTools Profiler
Забезпечує графічний інтерфейс для аналізу, які компоненти і коли рендеряться, скільки часу займають.
Плагіни (наприклад, why-did-you-render)
Існують бібліотеки, що можуть “підказати”, чому саме компонент повторно рендериться.
Підходи до мінімізації зайвих рендерів
memo, React.memo і PureComponent
React.memo (для функціональних компонентів) або PureComponent (для класових) — це механізми, що виконують поверхневе порівняння пропсів. Якщо пропси не змінились, компонент не рендеритиметься вдруге.
Приклад із React.memo:
const Child = React.memo(function Child({ data }) {
console.log('Child render');
return <div>{data.value}</div>;
});
У цьому випадку, якщо data
(як посилання) не змінилося, Child не буде рендеритись.
useCallback та useMemo
Якщо у батьківському компоненті під час кожного рендеру створювати анонімну функцію в пропсах, то дочірній компонент буде вважати, що пропс змінився. Щоб запобігти цьому, варто використовувати useCallback і useMemo.
Приклад:
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<Child onClick={handleClick} />
);
}
Тепер onClick
буде завжди тим самим посиланням, і якщо інші пропси не змінюються, Child не рендериться повторно.
Оптимізація контексту
Якщо контекст містить занадто багато даних, кожна зміна змушуватиме всі компоненти, що його використовують, рендеритись. Краще робити контекст більш “вузьким”, винести різні дані в різні контексти. Або використовувати бібліотеки на кшталт valtio, jotai чи Redux.
Приклад “вузького” контексту:
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Замість того, щоб зберігати інші дані, тримаємо тут лише theme
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Тепер зміна теми не впливає на інші дані, бо вони знаходяться в окремих контекстах.
Не оновлювати стан без потреби
Якщо в певний момент код викликає setState, який не змінює реальних значень, все одно відбудеться рендер. Перевірте, чи дійсно потрібно відправляти setState.
Використання “key” при списках
Іноді при відтворенні списків чи таблиць некоректна прив’язка key призводить до зайвого рендеру елементів.
Демонстрація
Нехай у нас є Parent, що підключає Child із пропом handleClick:
function Parent() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
return (
<div>
<Child onClick={increment} />
<p>Count: {count}</p>
</div>
);
}
const Child = React.memo(function Child({ onClick }) {
console.log('Child render');
return <button onClick={onClick}>Increment</button>;
});
Проблема: навіть з React.memo, Child буде рендеритись при кожному оновленні count, оскільки increment
створюється наново (нове посилання). Щоб це виправити, використовують useCallback:
function Parent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount(c => c + 1), []);
return (
<div>
<Child onClick={increment} />
<p>Count: {count}</p>
</div>
);
}
Тепер Child не буде перерисовуватись при зміні count, доки проп onClick лишається тим самим посиланням (і не змінюється, бо useCallback має порожній масив залежностей).
Загальний алгоритм
- Виявити зайві рендери.
- Перевірити, чи компонент має React.memo (якщо це функціональний компонент).
- Забезпечити незмінність пропів та стану там, де це можливо.
- Переглянути підходи до контексту (або використовувати бібліотеки з більш ефективним оновленням).
- Видалити зайві setState виклики.
- Мемоізувати анонімні функції, об’єкти.
Висновок
React успішно оптимізує рендеринг, проте зайві рендери можуть “з’їдати” продуктивність, особливо у складних додатках. Застосування таких інструментів, як React.memo, useCallback, useMemo, а також розумне використання контексту і дотримання чистоти стана допомагає уникнути багатьох непотрібних повторних відтворень. Аналіз своїх компонентів за допомогою DevTools і профайлінгу допоможе знайти ті місця, де варто застосувати оптимізацію, і зробити ваш React-код швидшим і ефективнішим.