# Оптимізація React: спочатку виміряй, потім мемоізуй

> Чому useMemo на все підряд не прискорює застосунок: як читати React DevTools Profiler, звідки насправді беруться ре-рендери і коли мемоізація дійсно потрібна. З прикладами до/після.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-09-24
- Категорія: React і Frontend
- Canonical: https://cookiesoftware.io/blog/optymizatsiia-react-performance

---

**Коротка відповідь.** Оптимізація React починається не з `useMemo`, а з вимірювання. Більшість «гальм» – це не повільний рендер, а зайві ре-рендери через невдалу структуру стану. Порядок дій: виміряй у Profiler → виправ структуру стану й композицію → і лише потім точково мемоізуй те, що справді дороге. Нижче – як це робити крок за кроком.

## Чому «обгорнути все в useMemo» не працює

На співбесідах я регулярно чую відповідь «якщо повільно – додам useMemo і React.memo». Це карго-культ: інструмент застосовують за формою, без розуміння причини.

Сама мемоізація не безкоштовна: React зберігає попередні значення, порівнює залежності на кожен рендер, а код стає важчим для читання. Якщо обчислення дешеве, а залежності змінюються щоразу – ти платиш за порівняння і нічого не заощаджуєш. Гірше: `useMemo` на кожному кроці маскує справжню проблему, через яку компонент взагалі рендериться занадто часто.

## Крок 1 – виміряй у React DevTools Profiler

Перш ніж щось міняти, отримай факти. У [React DevTools](https://react.dev/learn/react-developer-tools) є вкладка Profiler:

1. Натисни запис, виконай повільну дію (введення в пошук, відкриття списку), зупини запис.
2. Подивись на **commits** – кожен стовпчик це одне застосування змін до DOM. Багато довгих commits підряд під час набору тексту – типовий симптом.
3. У flame-графі видно, **які компоненти рендерилися і скільки часу зайняв кожен**. Сірі – не рендерилися взагалі.
4. Увімкни в налаштуваннях «Record why each component rendered» – Profiler покаже причину: змінилися props, state чи context.

Після цього в тебе не відчуття «щось повільно», а конкретний список: хто рендериться, як часто і чому. Оптимізувати без цього списку – стріляти в темряві.

## Звідки насправді беруться зайві ре-рендери

Три причини покривають майже всі випадки:

1. **Рендер батька = рендер дітей.** За замовчуванням React рендерить усе піддерево компонента, чий стан змінився. Якщо стан пошукового поля живе у корені сторінки – кожна літера перерендерює всю сторінку.
2. **Нові об'єкти в пропсах на кожен рендер.** `style={{ color: 'red' }}`, `onClick={() => ...}`, `items={data.filter(...)}` – це щоразу нові референси. Для звичайних компонентів це не проблема (вони й так рендеряться разом із батьком), але це зводить нанівець `React.memo` у дітей.
3. **Широкий контекст.** Один великий context із десятком полів перерендерює всіх споживачів при зміні будь-якого поля.

Зверни увагу: жодна з причин не лікується «додати useMemo всюди». Перша лікується структурою стану, друга – точковою стабілізацією референсів, третя – розбиттям контексту.

## Структура стану важливіша за мемоізацію

Найефективніша «оптимізація» в React – тримати стан якомога ближче до місця використання. Класичний приклад: пошукове поле над великим списком.

До – стан у корені, кожна літера рендерить усе:

```tsx
function Page({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <HeavyList items={items} query={query} />
      <Sidebar />   {/* рендериться на кожну літеру без потреби */}
      <Footer />    {/* теж */}
    </>
  );
}
```

Після – стан спущено у компонент, якому він потрібен:

```tsx
function Page({ items }: { items: Item[] }) {
  return (
    <>
      <SearchableList items={items} />  {/* стан query живе тут */}
      <Sidebar />
      <Footer />
    </>
  );
}
```

Тепер набір тексту рендерить лише `SearchableList`. Нуль мемоізації – а ефект більший, ніж від `React.memo` на кожному компоненті.

Другий прийом – **композиція через children**. Якщо компоненту зі станом потрібно обгортати важкий вміст, передай вміст як `children`: React не перерендерює елементи, створені поза компонентом, чий стан змінився:

```tsx
function Collapsible({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Розгорнути</button>
      {open && children}  {/* children створені батьком – не рендеряться повторно */}
    </div>
  );
}
```

Детальніше про те, що саме React порівнює між рендерами, – у статті про [reconciliation і keys](/blog/react-reconciliation-keys).

## Коли useMemo і memo справді потрібні

Після виправлення структури залишаються два чесні сценарії:

**1. Справді дороге обчислення.** Фільтрація чи агрегація великого масиву на кожен рендер:

```tsx
const visibleRows = useMemo(
  () => rows.filter(matchesFilters).sort(byColumn(sortKey)),
  [rows, matchesFilters, sortKey]
);
```

Критерій «дорого» – не інтуїція, а Profiler: якщо обчислення займає одиниці мілісекунд на реальних даних, мемоізація не потрібна.

**2. Стабільні пропси для memo-компонента.** Якщо важкий дочірній компонент обгорнуто в `React.memo`, усі його пропси мають бути стабільними, інакше memo не спрацює жодного разу:

```tsx
const handleSelect = useCallback((id: string) => setSelected(id), []);

return <HeavyTable rows={visibleRows} onSelect={handleSelect} />;
```

Правило пари: `React.memo` без стабілізованих пропсів – мертвий вантаж; `useCallback` без memo-споживача – теж. Вони працюють лише разом.

## Карго-культ: як виглядає безглузда мемоізація

```tsx
// Порівняння залежностей коштує більше, ніж сама конкатенація
const fullName = useMemo(() => `${first} ${last}`, [first, last]);

// useCallback без жодного memo-споживача нижче
const onClick = useCallback(() => setOpen(true), []);
<PlainButton onClick={onClick} />
```

Якщо на співбесіді тебе спитають «навіщо тут useMemo», а відповідь буде «про всяк випадок» – це мінус, а не плюс. Уміння пояснити, чому мемоізації **немає**, цінується не менше: про це є окремий розбір у [гайді з підготовки до React-співбесіди](/blog/react-interview-guide).

## Довгі списки: коли структура вже не рятує

Окремий клас проблем – списки на сотні й тисячі рядків. Тут навіть ідеально мемоізований рядок не допоможе: браузер фізично тримає в DOM тисячі вузлів, і перший рендер повільний незалежно від React.

Рішення – **віртуалізація**: рендерити лише видимі рядки плюс невеликий буфер. Це десятки елементів у DOM замість тисяч. На співбесіді достатньо пояснити принцип (контейнер із фіксованою висотою, абсолютне позиціювання видимого вікна, перерахунок на скрол) і чесно сказати, що в продакшні береш готову бібліотеку, а не пишеш власну.

І суміжний нюанс, який часто перевіряють: некоректні `keys` у списках змушують React перестворювати DOM-вузли замість повторного використання. `key={index}` при вставці на початок списку – класична причина «повільного списку», яку не видно у flame-графі напряму. Це тема окремої статті про [reconciliation і keys](/blog/react-reconciliation-keys).

## Три питання про продуктивність, які ставлять на співбесідах

**– Чому React.memo не допоміг?**
Найчастіше тому, що хоча б один проп – нестабільний референс: інлайн-функція, новий об'єкт або масив із `filter`/`map` у рендері. `memo` порівнює пропси поверхнево; один новий референс – і порівняння програно.

**– Чим useMemo відрізняється від useCallback?**
`useMemo` кешує результат виклику функції, `useCallback` – саму функцію. `useCallback(fn, deps)` еквівалентний `useMemo(() => fn, deps)`. Обидва мають сенс лише коли стабільність значення хтось використовує: залежності іншого хука або memo-компонент.

**– З чого почнеш, якщо «сторінка гальмує»?**
Сильна відповідь завжди починається з вимірювання, а не з інструмента: Profiler → знайти найчастіші/найдовші commits → з'ясувати причину рендерів → виправити структуру → точкова мемоізація → повторне вимірювання.

## Чекліст діагностики продуктивності

1. Виміряй у Profiler: які компоненти рендеряться і чому.
2. Чи можна спустити стан нижче або передати важкий вміст через children?
3. Чи не створюєш нові об'єкти/масиви/функції у пропсах memo-компонентів?
4. Чи не занадто широкий context? Розбий за призначенням.
5. Для довгих списків – чи потрібна віртуалізація (рендер лише видимих рядків)?
6. Лише тепер: точковий `useMemo` для дорогих обчислень, `memo` + стабільні пропси для важких піддерев.
7. Повтори вимірювання і порівняй commits до/після. Без «після» оптимізація не вважається зробленою.

Уміння пройти цей чекліст уголос – одна з найсильніших відповідей на архітектурному раунді: як саме будувати таку аргументацію, розбираю в статті про [React-архітектуру на Senior-співбесіді](/blog/senior-react-architecture).
