Боротьба з перерендерами у React: збереження продуктивності UI

React

React дозволяє створювати інтерфейси, які оновлюються лише тоді, коли дані справді змінюються. Але на практиці трапляються випадки, коли компоненти рендеряться знову й знову, навіть без реальної потреби. Це може знижувати продуктивність, спричиняти “пригальмовування” та сповільнювати відгук додатка. У цій статті зібрано ключові прийоми та концепції, які допоможуть вам ефективно боротися з надлишковими перерендерами і зберігати високу швидкодію UI.

Чому перерендери важливі

React працює за принципом Virtual DOM, порівнюючи попередній стан з новим та оновлюючи інтерфейс лише там, де відбулися зміни. Але якщо компонент “вважає”, що змінився його проп або стан, відбувається повторний рендер. Якщо таких повторів багато, UI може почати гальмувати. Особливо це помітно у великих таблицях, списках або багаторівневих компонентах.

Типові причини надлишкових рендерів

  1. Анонімні функції та об’єкти у пропсах
    Кожного разу при рендері батьківський компонент створює нове посилання (наприклад, () => setState(…)), яке React бачить як “зміна пропів” у дочірнього компонента.

  2. Зміна контексту (Context)
    Коли в контексті оновлюється навіть одне поле, всі компоненти, які його споживають, можуть перерендеритися.

  3. Відсутність мемоізації
    Якщо підкомпоненти не “захищені” за допомогою React.memo або PureComponent, вони можуть рендеритись навіть при незначних змінах у батьківському компоненті.

  4. Фальшиві оновлення стану
    Коли викликається setState з тим самим значенням (наприклад, setCount(count)), React теж ініціює рендер (хоча він може оптимізувати, якщо ви користуєтесь певними підходами).

Інструменти аналізу

  1. DevTools Profiler
    Chrome DevTools (Performance) або профайлер у React DevTools дають змогу побачити, які компоненти рендеряться і скільки часу це займає.

  2. why-did-you-render
    Спеціальна бібліотека, яка вказує у консоль, коли React виконує перерендер компонента і чому він вважає, що пропи змінились.

Мемоізація компонентів

React.memo

Для функціональних компонентів існує React.memo, який проводить поверхневе порівняння пропів:

const MyComponent = React.memo(function MyComponent(props) {
  console.log('Render MyComponent');
  return <div>{props.value}</div>;
});

Якщо props.value не змінився (поверхнево), MyComponent не буде ререндеритися. Проте це працює лише, якщо пропи є простими (числа, рядки). Для об’єктів чи масивів, які завжди створюються заново, React.memo не допоможе без додаткових прийомів.

useCallback і useMemo

Якщо передаємо у пропсах функції чи складні об’єкти, бажано використовувати useCallback і useMemo, щоб React “запам’ятав” (мемоізував) значення. Наприклад:

function Parent() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => setCount(c => c + 1), []);

  return <Child onClick={increment} />;
}

Тепер onClick завжди має те саме посилання на функцію, і якщо Child обгорнуто в React.memo, він не ререндеритиметься при зміні count (якщо на інші пропи це не впливає).

Оптимізація контексту

Якщо контекст (React Context) містить багато даних або часто змінюється, кожен Consumer може ререндеритися. Один зі способів:

  • Розділити контекст на кілька “вужчих” контекстів, щоб при зміні певної частини не рендерились інші споживачі.
  • Або використовувати useContext в поєднанні зі спеціальними техніками (наприклад, select функціями) чи бібліотеками (react-tracked), які відстежують конкретні поля, що змінилися.

Захист від “фальшивих” оновлень

Перевірка стану перед викликом setState

function Parent() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prev => {
      if (prev === 100) return prev; // Якщо досягнуто межі, не оновлюємо
      return prev + 1;
    });
  };

  return <button onClick={increment}>{count}</button>;
}

У такому разі, якщо поточний стан уже 100, ми не викликаємо непотрібний рендер.

Використання бібліотек

Immer чи valtio

Для складних даних у стані можна використовувати такі бібліотеки, щоб грамотно робити immutable-оновлення, не створюючи “зайвих” нових об’єктів. Це зменшує ризик випадкових перезаписів, що провокують ререндер.

MobX

MobX автоматично “прив’язує” компоненти до певних спостережуваних (observable) полів. Якщо поле не змінювалося, компонент не ререндериться. Проте це вимагає додаткових архітектурних рішень, а також знання MobX.

Поради та шаблони

Split Rendering

Якщо у батьківському компоненті є багато дочірніх, краще “розділити” логіку так, щоб дочірні компоненти отримували тільки ті пропи, які їм дійсно потрібні. Таким чином, якщо змінюється якась глобальна змінна, яку не використовує конкретний дочірній компонент, він не рендериться.

Використання List (для списків) із memo та key

Якщо у вас є довгий список (наприклад, відображення сотень чи тисяч елементів), зверніть увагу на:

  1. Правильне встановлення key для кожного елементу.
  2. Мемоізацію кожного рядка (якщо він не залежить від глобальних змін).
  3. Можливо, потрібно застосувати “віртуальний список” (react-window чи react-virtualized), щоб малювати тільки видимі елементи.

Контрольний список

  1. Використовуйте React.memo для компонентів, які отримують пропи, що нечасто змінюються.
  2. Мемоізуйте об’єкти й функції через useCallback та useMemo, щоб уникати створення нових посилань.
  3. Розділяйте контекст (Context) на менші частини, щоб оновлення стосувалось лише тих компонентів, які дійсно мають змінитись.
  4. Перевіряйте “фальшиві” оновлення стану (не викликайте setState, якщо немає реальних змін).
  5. Використовуйте інструменти аналізу: React DevTools Profiler, why-did-you-render.
  6. Розглядайте віртуалізацію списків, коли працюєте з великими масивами даних.

Висновок

Зайві перерендери у React можуть “з’їдати” продуктивність, але є багато способів боротися з ними. Головне — зрозуміти, чому відбуваються ці повторні рендери: чи це через передачу функцій-анонімок, великих об’єктів у пропсах, чи, можливо, контекст оновлюється надто часто. Правильне використання React.memo, useCallback, useMemo, архітектурне розділення стану, оптимізація контексту — усе це зменшить кількість непотрібних рендерів. У підсумку, користувачі отримають швидкі й чуйні інтерфейси, а код стане більш передбачуваним і зручним у підтримці.