Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Техніки оптимізації продуктивності React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React performance optimization (оптимізація продуктивності React)** - набір технік для усунення зайвих ре-рендерів, кешування важких обчислень і зменшення розміру бандлу. ```tsx const Child = React.memo(({ count }: { count: number }) => <div>{count}</div>); const stats = useMemo(() => computeStats(salesData), [salesData]); const handleExport = useCallback(() => exportToCSV(stats), [stats]); ``` **Головне:** спочатку профілюй через React DevTools Profiler, потім застосовуй `React.memo`, `useMemo`, віртуалізацію або code splitting тільки там, де дані показують реальну проблему.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**React performance optimization (оптимізація продуктивності React)** - це набір цільових технік (мемоізація, розбиття коду, віртуалізація), які запобігають зайвим ре-рендерам, пропускають важкі обчислення і зменшують початковий розмір бандлу у великих додатках. ## Теорія ### TL;DR - React ре-рендерить кожен дочірній компонент, коли змінюється стан батька; оптимізація додає ворота порівняння, щоб пропускати незмінені - `React.memo` кешує результат рендеру компонента; `useMemo` кешує обчислене значення; `useCallback` кешує посилання на функцію - Спочатку профілюй через React DevTools Profiler. Колокація стану і розбиття коду зазвичай дають більший ефект, ніж `memo` скрізь - Список з 200+ елементів → віртуалізація. Бандл більше 500KB → `React.lazy`. Обробник викликає ре-рендери дитини → `useCallback` + `React.memo` ### Короткий приклад ```tsx // Без оптимізації: Child ре-рендериться при кожній зміні count function Parent() { const [count, setCount] = useState(0); return <Child name="Item" count={count} />; } // З React.memo: Child пропускає ре-рендер, якщо props не змінились (поверхневе порівняння) const Child = React.memo(({ name, count }: { name: string; count: number }) => { console.log(`${name} rendered`); // Виводить тільки при реальній зміні props return <div>{name}: {count}</div>; }); ``` Parent ре-рендериться при кожному кліку. Мемоізований Child виводить лог тільки коли `name` або `count` реально змінюються. Без `React.memo` - при кожному рендері. ### Чому React ре-рендерить за замовчуванням Алгоритм узгодження (reconciliation) React ре-рендерить все піддерево під компонентом, чий стан або props змінились. Для маленьких дерев це непомітно. Для дашборду з 50+ дочірніми компонентами - накопичується швидко. Техніки оптимізації не змінюють алгоритм. Вони додають ворота з поверхневим порівнянням перед запуском функції компонента. Якщо props збігаються - React не торкається JSX і DOM. ### Коли використовувати кожну техніку - **Батько ре-рендериться часто, props дитини стабільні** → `React.memo` - **Важкі похідні дані: reduce, map, sort** → `useMemo` - **Обробник передається як prop у мемоізовану дитину** → `useCallback` - **Список з 200+ елементів** → `react-window` або `@tanstack/react-virtual` - **Початковий бандл більше 500KB** → `React.lazy` + `Suspense` - **Зміна стану ре-рендерить далекі частини дерева** → колокація стану ### Таблиця порівняння | Техніка | Що кешує | Коли перезапускається | Вплив на бандл | Доступна з | |---------|---------|----------------------|----------------|------------| | `React.memo` | Весь рендер компонента | Зміна props (поверхнево) | Нема | React 16.6 | | `useMemo` | Будь-яке обчислене значення | Зміна масиву залежностей | Нема | React 16.8 | | `useCallback` | Посилання на функцію | Зміна масиву залежностей | Нема | React 16.8 | | `React.lazy` | Чанк коду | Перший `import()` | Зменшує початковий | React 16.6 | | `react-window` | Видимі елементи списку | Скрол / viewport | ~10KB | Бібліотека | `useMemo` і `useCallback` - один механізм. `useCallback(fn, deps)` - скорочення для `useMemo(() => fn, deps)`. ### Як React виконує мемоізацію всередині Fiber-архітектура React пакетує оновлення стану через планувальник (scheduler) на основі `requestIdleCallback`. На фазі commit мемоізовані компоненти проходять перевірку, схожу на `shouldComponentUpdate`. Всередині - поверхневий diff через функцію `areEqual`. Якщо всі props збігаються - React пропускає JSX-обхід і зміни DOM повністю. V8 теж виграє від цього. Стабільні посилання на функції з `useCallback` дозволяють рушію повторно використовувати вже скомпільовані замикання (closure) замість виділення нових при кожному рендері. ### Типові помилки **Помилка 1: Відсутні залежності в `useMemo` або `useCallback`** ```tsx // Застарілі дані: items змінюється, але doubled ніколи не оновлюється const doubled = useMemo(() => items.map(i => i * 2), []); // Виправлення: додати залежність const doubled = useMemo(() => items.map(i => i * 2), [items]); ``` ESLint-правило `react-hooks/exhaustive-deps` ловить це автоматично. Це найпоширеніша скарга на r/reactjs щодо хуків. **Помилка 2: Передача нових об'єктів або функцій у мемоізовану дитину** ```tsx // Ре-рендер при кожному рендері батька - config щоразу новий об'єкт const Child = React.memo(({ config }) => <div>{config.theme}</div>); <Child config={{ theme: "dark", size: 14 }} /> // ❌ // Виправлення: мемоізуй об'єкт або передавай примітиви const config = useMemo(() => ({ theme: "dark", size: 14 }), []); <Child config={config} /> // ✅ ``` Поверхневе порівняння перевіряє посилання, не вміст. Новий об'єктний літерал не проходить перевірку, навіть якщо всередині нічого не змінилось. Саме це стоїть за більшістю питань на StackOverflow про `React.memo` "який не працює". **Помилка 3: `useCallback` без мемоізації дитини** ```tsx // handleClick стабільний, але Child все одно ре-рендериться const handleClick = useCallback(() => console.log("click"), []); <Child onClick={handleClick} /> // Child без React.memo ``` `useCallback` сам по собі нічого не дає для запобігання ре-рендерам. Дитина теж потребує `React.memo`, інакше стабільне посилання просто ігнорується. **Помилка 4: Мемоізація всього підряд** Додавання `React.memo` до кожного компонента збільшує накладні витрати порівняння при кожному рендері. Для компонентів, що рендеряться рідко або отримують прості примітиви, ці витрати можуть перевищити економію. Дані з профайлера у блозі Дена Абрамова показують +15% CPU від надмірної мемоізації. Застосовуй memo тільки там, де DevTools Profiler показує реальну проблему. ### Де зустрічається в реальних проектах - **Next.js**: `React.lazy` / `Suspense` для code splitting на рівні маршрутів; `/dashboard` завантажується тільки при першому відвідуванні - **Redux Toolkit**: `createSelector` з Reselect обгортає логіку `useMemo` для кешування похідного стану - **Material-UI**: `React.memo` на `ListItem` для пропуску ре-рендерів у drawer зі 100+ елементами - **TanStack Query**: `useQuery` автоматично мемоізує дані запиту, тому компоненти ре-рендеряться тільки при реальних змінах - **Vercel commerce template**: `useMemo` для підрахунку кошика, `useCallback` для обробників оформлення замовлення ### Питання на співбесіді **Q:** Коли `React.memo` не запобігає ре-рендеру? **A:** Коли компонент споживає context, що змінився, або коли батько викликає `forceUpdate`. Також коли props містять об'єкти або функції, створені inline, бо поверхневе порівняння бачить їх як нові значення кожного разу. **Q:** У чому різниця між `useMemo` і `useCallback`? **A:** `useMemo` кешує будь-яке значення: об'єкт, масив, результат обчислення. `useCallback` - скорочення для `useMemo(() => fn, deps)`. Використовуй `useCallback` коли кешоване значення є функцією, `useMemo` коли це дані. **Q:** Як оптимізувати список з 10,000 елементів? **A:** Віртуалізація через `react-window` або `@tanstack/react-virtual`. Рендеряться тільки видимі рядки - приблизно 9-15 замість 10,000. Пам'ять скорочується приблизно в 100 разів, скрол залишається на 60fps там, де невіртуалізований список падає до 15fps або нижче. **Q:** У чому різниця між `useTransition` і `useMemo`? **A:** Різні задачі. `useMemo` пропускає обчислення. `useTransition` позначає оновлення стану як не термінове, щоб React міг перервати його на користь терміновіших дій користувача. Використовуй обидва разом у пошуку в реальному часі: `useMemo` для фільтрації, `useTransition` щоб input залишався чуйним. **Q:** (Senior) Як планувальник взаємодіє з мемоізованими компонентами в Concurrent Mode? **A:** Низькопріоритетні оновлення через `useTransition` можуть бути перервані посередині дерева. Якщо прийде більш пріоритетне оновлення - React може пропустити рендер мемоізованих компонентів. Перевірка memo все одно запускається, але компонент може ре-рендеритись знову, коли планувальник відновить перервану роботу. Виміряти це можна через `getCurrentPriorityLevel()` з пакету React scheduler. ## Приклади ### Дашборд з useMemo і useCallback Дашборд продажів, який перераховує підсумки тільки при зміні даних і передає стабільний обробник експорту в дочірній тулбар. ```tsx function SalesDashboard({ salesData }: { salesData: Sale[] }) { // Перераховується тільки при зміні посилання salesData const stats = useMemo(() => { const total = salesData.reduce((sum, s) => sum + s.amount, 0); return { total, avg: total / salesData.length, count: salesData.length, }; }, [salesData]); // Стабільне посилання: StatsPanel пропускає ре-рендер при рендері батька const handleExport = useCallback(() => { exportToCSV(stats); }, [stats]); return ( <div> <StatsPanel stats={stats} onExport={handleExport} /> <SalesTable data={salesData} /> </div> ); } ``` Без `useMemo` - `stats` перераховується при кожному рендері, навіть коли `salesData` не змінився. На реальному дашборді з анімаціями і оновленнями кожні 30 секунд це перетворюється на сотні зайвих обчислень за сесію. ### Віртуалізований список з react-window Рендер 10,000 товарів без підвисань браузера. ```tsx import { FixedSizeList as List } from "react-window"; const VirtualizedList = React.memo(({ items }: { items: Item[] }) => ( <List height={600} // Фіксована висота контейнера в px itemCount={items.length} itemSize={70} // Висота кожного рядка в px width="100%" > {({ index, style }) => ( // style містить абсолютне позиціювання від react-window <div style={style}>{items[index].name}</div> )} </List> )); ``` З 10,000 елементів по 70px загальна висота прокрутки - 700,000px. React-window рендерить тільки ~9 видимих рядків у будь-який момент. Chrome Profiler показує скрол на 60fps там, де невіртуалізований список падає до 15fps або нижче. ### Колокація стану для запобігання ре-рендерам всього дерева Переміщення стану наведення вниз до компонента, якому він насправді потрібен. ```tsx // ❌ Стан наведення в App: всі діти ре-рендеряться при кожному русі миші function App() { const [hoveredId, setHoveredId] = useState<string | null>(null); return <ProductList items={products} hoveredId={hoveredId} onHover={setHoveredId} />; } // ✅ Стан наведення в самому елементі: ре-рендериться тільки він function ProductCard({ product }: { product: Product }) { const [isHovered, setIsHovered] = useState(false); return ( <div onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className={isHovered ? "highlighted" : ""} > {product.name} </div> ); } ``` На практиці це часто найбільший виграш. Перед тим як тягнутись до `memo`, перевір чи стан, що викликає ре-рендери, справді має жити так високо. Я бачив, як дерево з 40 компонентами скорочувало ре-рендери до одиниць просто переміщенням одного `useState` на два рівні нижче.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.