Пошук уроків, статей та іншого контенту
Систематизуємо підходи до продуктивності React: вимірювання, оптимізацію рендерингу, мережі, стану та бандла.
Продуктивність React — це не максимальна кількість memo, а зменшення вартості роботи, яку браузер і React виконують під час:
початкового завантаження;
оновлення стану;
повторного рендерингу компонентів;
виконання JavaScript;
побудови DOM і layout;
завантаження даних та інших ресурсів.
Оптимізацію варто починати з вимірювання. Перед зміною коду потрібно знати:
Яка операція повільна.
Скільки часу вона займає.
Як часто вона виконується.
Чи справді проблема впливає на користувача.
Мікрооптимізація компонента, який рендериться один раз, зазвичай не дає помітного результату. Натомість зайвий рендер великого списку під час кожного натискання клавіші може суттєво погіршити взаємодію.
React DevTools Profiler показує:
які компоненти рендерилися;
скільки часу зайняв кожен рендер;
що спричинило оновлення;
які компоненти рендерилися разом із батьківським компонентом.
Профілювати потрібно production-збірку або середовище, максимально наближене до production. Development-режим може додатково перевіряти код і створювати враження повільнішої роботи.
ProfilerReact має API Profiler, який дозволяє отримувати дані про рендеринг окремої частини дерева.
import { Profiler, useMemo, useState } from 'react';
function onRender(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
});
}
function ProductList({ products, query }) {
const visibleProducts = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
return products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
}, [products, query]);
return (
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>
{product.name} — {product.price} грн
</li>
))}
</ul>
);
}
export default function App() {
const [query, setQuery] = useState('');
const [products] = useState([
{ id: 1, name: 'Keyboard', price: 2500 },
{ id: 2, name: 'Mouse', price: 1200 },
{ id: 3, name: 'Monitor', price: 9000 },
]);
return (
<main>
<label>
Пошук:
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</label>
<Profiler id="ProductList" onRender={onRender}>
<ProductList products={products} query={query} />
</Profiler>
</main>
);
}actualDuration — фактичний час поточного оновлення, а baseDuration — приблизна вартість рендеру без оптимізацій мемоізації. Ці дані корисні для діагностики, але не повинні бути єдиним джерелом висновків.
Для повної картини використовуйте:
Performance у браузерних DevTools — JavaScript, layout, paint і довгі задачі;
Network — розмір ресурсів, час завантаження та порядок запитів;
Coverage — код JavaScript і CSS, який фактично не використовується;
Lighthouse або подібні інструменти — загальні метрики завантаження.
Особливо важливо перевіряти продуктивність на повільному процесорі, повільній мережі та мобільному пристрої. Потужний локальний комп’ютер може приховати проблеми.
Стан потрібно розміщувати якомога ближче до компонентів, які його використовують. Якщо стан пошуку потрібен лише списку товарів, не обов’язково піднімати його до кореневого компонента застосунку.
Чим ближче стан до споживачів, тим менше компонентів потенційно оновлюється після зміни цього стану.
function SearchableProducts({ products }) {
const [query, setQuery] = useState('');
const visibleProducts = useMemo(() => {
const normalizedQuery = query.toLowerCase();
return products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
}, [products, query]);
return (
<>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Пошук товарів"
/>
<ProductList products={visibleProducts} />
</>
);
}Не кожен повторний рендер є проблемою. React може швидко виконати повторний рендер невеликого компонента. Оптимізація потрібна тоді, коли вимірювання показує значну вартість або погану взаємодію.
React.memomemo дозволяє пропустити рендер компонента, якщо його props не змінилися за поверхневим порівнянням.
import { memo } from 'react';
const ProductRow = memo(function ProductRow({ product, onSelect }) {
return (
<li>
<span>{product.name}</span>
<button onClick={() => onSelect(product.id)}>
Обрати
</button>
</li>
);
});memo не допоможе, якщо батьківський компонент щоразу створює нові об’єкти або функції:
<ProductRow
product={{ id: product.id, name: product.name }}
onSelect={(id) => handleSelect(id)}
/>У цьому випадку props мають нові посилання під час кожного рендеру.
Стабільніші props можуть виглядати так:
<ProductRow
product={product}
onSelect={handleSelect}
/>Однак memo має власну вартість порівняння props. Його не слід додавати до всіх компонентів автоматично.
useCallbackuseCallback зберігає посилання на функцію між рендерами, якщо залежності не змінилися.
const handleSelect = useCallback((productId) => {
setSelectedProductId(productId);
}, []);Це може бути корисно, коли:
функція передається мемоізованому дочірньому компоненту;
функція є залежністю іншого hook;
створення нової функції спричиняє вимірювану проблему.
Якщо функція передається звичайному компоненту і не впливає на дорогі обчислення, useCallback часто лише ускладнює код.
useMemouseMemo кешує результат обчислення між рендерами.
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.price - b.price);
}, [products]);Важливі правила:
не змінюйте початковий масив через sort, reverse або splice;
вказуйте всі значення, від яких залежить обчислення;
не використовуйте useMemo для кожного простого виразу;
кеш не є гарантією збереження значення назавжди.
useMemo доречний для дорогих обчислень або стабілізації значення, яке передається мемоізованому компоненту.
Навіть якщо об’єкт логічно має ті самі дані, новий об’єкт має інше посилання:
// Новий об’єкт під час кожного рендеру
<Panel options={{ dense: true }} />Якщо стабільність посилання справді важлива, значення можна винести за межі компонента:
const panelOptions = { dense: true };
function App() {
return <Panel options={panelOptions} />;
}Але це потрібно робити лише для оптимізації, яку підтверджено профілюванням.
Великий компонент, який одночасно містить поле пошуку, список, статистику і форму, може оновлювати всі частини після зміни одного поля.
Розділення на менші компоненти допомагає:
локалізувати стан;
використовувати memo лише там, де це потрібно;
простіше аналізувати профіль;
уникати зайвого рендеру незалежних частин інтерфейсу.
useDeferredValueuseDeferredValue дозволяє відкласти оновлення частини інтерфейсу. Це корисно, коли введення має залишатися швидким, а відображення результатів є дорогим.
import { useDeferredValue, useMemo, useState } from 'react';
function SearchPage({ products }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
const normalizedQuery = deferredQuery.trim().toLowerCase();
return products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
}, [products, deferredQuery]);
return (
<>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву товару"
/>
<p>
Поточний запит: {query}
</p>
<ul>
{results.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</>
);
}Користувач може бачити, що поле оновилося раніше за список. Це нормальна поведінка для відкладеного оновлення.
useDeferredValue не скасовує саме обчислення. Він змінює пріоритет оновлення. Якщо обчислення дуже важке, його також потрібно оптимізувати або змінити архітектуру даних.
useTransitionuseTransition позначає оновлення як непершочергове і дозволяє показати стан очікування.
import { useState, useTransition } from 'react';
function Filters({ onChange }) {
const [isPending, startTransition] = useTransition();
function handleChange(event) {
const value = event.target.value;
startTransition(() => {
onChange(value);
});
}
return (
<>
<select onChange={handleChange} defaultValue="">
<option value="">Усі категорії</option>
<option value="books">Книги</option>
<option value="electronics">Електроніка</option>
</select>
{isPending && <span> Оновлення…</span>}
</>
);
}Перехід не робить повільну операцію швидшою. Він допомагає React не блокувати термінові оновлення, наприклад введення тексту або натискання кнопки.
Рендер тисяч елементів створює значне навантаження на:
React reconciliation;
кількість DOM-вузлів;
layout і paint;
пам’ять браузера.
Для великих списків застосовують віртуалізацію: у DOM перебувають лише елементи, видимі у viewport, а не весь набір даних.
Важливі додаткові правила:
використовуйте стабільний key, пов’язаний із сутністю;
не використовуйте індекс масиву як key, якщо порядок елементів може змінюватися;
не виконуйте складне форматування для кожного рядка на кожен рендер;
не передавайте кожному рядку нові великі об’єкти без потреби.
items.map((item) => (
<Row key={item.id} item={item} />
))Віртуалізацію слід додавати для справді великих або дорогих списків, а не для кожного короткого набору елементів.
Глобальний стан, який змінюється часто, може спричиняти оновлення великої частини дерева. Наприклад, значення поля пошуку не варто зберігати глобально, якщо його використовує лише один екран.
Локалізуйте:
стан відкриття меню;
значення тимчасового вводу;
стан вибраного рядка;
локальні помилки форми;
стан завантаження окремої секції.
Оновлення значення Context може спричинити рендер компонентів, які читають цей контекст. Не створюйте один контекст для всього стану застосунку, якщо його частини змінюються з різною частотою.
Замість одного великого значення:
<StoreContext.Provider value={{ user, theme, cart }}>
{children}
</StoreContext.Provider>можна розділити незалежні області:
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<CartContext.Provider value={cart}>
{children}
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>Об’єкт value також може отримувати нове посилання під час кожного рендеру провайдера. Якщо це спричиняє зайві оновлення, стабілізуйте значення або розділіть провайдери.
Поганий запит може передавати всі поля і всі записи, хоча екрану потрібні лише кілька.
Для продуктивності важливі:
пагінація;
серверна фільтрація;
сортування на сервері для великих наборів;
вибір лише необхідних полів;
кешування повторних запитів;
скасування застарілих запитів.
Пошук, який виконує запит на кожне натискання клавіші, зазвичай потребує debounce або іншої стратегії керування частотою запитів. Водночас затримка не повинна бути способом приховати повільний API.
Якщо користувач швидко змінює пошуковий запит, відповідь для старого запиту може прийти після відповіді для нового. Компонент повинен ігнорувати або скасовувати застарілі запити.
import { useEffect, useState } from 'react';
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
const controller = new AbortController();
async function loadResults() {
try {
setError(null);
const response = await fetch(
`/api/products?q=${encodeURIComponent(query)}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('Не вдалося завантажити результати');
}
const data = await response.json();
setResults(data);
} catch (requestError) {
if (requestError.name !== 'AbortError') {
setError(requestError);
}
}
}
loadResults();
return () => {
controller.abort();
};
}, [query]);
if (error) {
return <p>Помилка завантаження</p>;
}
return (
<ul>
{results.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Скасування не лише економить мережеві ресурси, а й запобігає показу неправильних даних.
Якщо два запити не залежать один від одного, їх не слід виконувати послідовно:
const [products, categories] = await Promise.all([
fetch('/api/products').then((response) => response.json()),
fetch('/api/categories').then((response) => response.json()),
]);Якщо другий запит потребує результату першого, послідовність є необхідною. Оптимізація повинна зберігати коректність залежностей.
Не весь JavaScript потрібен під час першого завантаження. Великий або рідко використовуваний екран можна завантажити окремим chunk через lazy.
import { lazy, Suspense, useState } from 'react';
const ReportsPage = lazy(() => import('./ReportsPage'));
export default function App() {
const [showReports, setShowReports] = useState(false);
return (
<main>
<button onClick={() => setShowReports(true)}>
Відкрити звіти
</button>
{showReports && (
<Suspense fallback={<p>Завантаження звітів…</p>}>
<ReportsPage />
</Suspense>
)}
</main>
);
}Code splitting особливо корисний для:
рідкісних маршрутів;
адміністративних розділів;
складних редакторів;
великих бібліотек, потрібних лише на окремому екрані.
Не потрібно розділяти кожен маленький компонент. Надмірна кількість chunk може збільшити кількість мережевих запитів і ускладнити завантаження.
Регулярно перевіряйте:
які залежності займають найбільше місця;
чи не підключено дві версії однієї бібліотеки;
чи імпортуються невикористані модулі;
чи не потрапляють серверні або development-залежності в production-бандл;
чи завантажуються великі модулі на старті без потреби.
Використовуйте production-збірку та інструмент аналізу бандла, який підтримує ваш bundler. Орієнтуйтеся не лише на розмір файлу, а й на реальний час виконання JavaScript на цільовому пристрої.
Якщо бібліотека підтримує tree-shaking, модульні імпорти можуть дозволити видалити невикористаний код:
import { format } from 'date-fns';Фактичний результат залежить від конкретної бібліотеки, способу її публікації та налаштувань збірки. Не робіть висновок лише за синтаксисом імпорту — перевіряйте production-бандл.
memo, useMemo і useCallback не є універсальними прискорювачами. Вони додають складність, порівняння залежностей і потребу підтримувати коректний список залежностей.
Спочатку визначте проблему профайлером, а потім перевірте, чи зміна справді її зменшила.
Пропущена залежність може спричинити використання застарілого значення. Зайва залежність може змусити обчислення або ефект виконуватися частіше.
Не вимикайте правила перевірки залежностей лише для того, щоб прибрати попередження. Змініть структуру коду так, щоб залежності були явними.
key// Небезпечно для списку, який може сортуватися, фільтруватися або змінюватися
items.map((item, index) => (
<Row key={index} item={item} />
))Після вставки або видалення елемента React може зіставити компонент із неправильними даними. Використовуйте стабільний ідентифікатор сутності.
useMemo не зменшить розмір HTTP-відповіді, а memo не скасує непотрібний запит. Розділяйте проблеми:
дані завеликі — змінюйте API або спосіб завантаження;
JavaScript завеликий — аналізуйте бандл і code splitting;
рендер дорогий — аналізуйте дерево та обчислення;
взаємодія блокується — перевіряйте довгі задачі та пріоритет оновлень.
Development-режим, зокрема додаткові перевірки, може мати іншу поведінку та продуктивність. Перевіряйте результат у production-збірці, але не ігноруйте повільність development, якщо вона заважає щоденній роботі.
Визначте повільний сценарій з боку користувача.
Відтворіть його на типовому або слабшому пристрої.
Виміряйте React-рендер через Profiler.
Перевірте Performance та Network у DevTools.
Визначте тип проблеми: рендер, обчислення, мережа, JavaScript або DOM.
Зробіть одну цільову зміну.
Повторіть вимірювання.
Перевірте, що функціональність і читабельність не погіршилися.
Зафіксуйте важливі результати в performance-бюджеті або тестах.
Оптимізацію React починають із вимірювання, а не з механічного додавання hooks.
Локальний стан зменшує область оновлень.
memo працює лише за стабільних props і не потрібен кожному компоненту.
useMemo призначений для дорогих обчислень або стабілізації значень.
useCallback корисний переважно у взаємодії з мемоізованими компонентами.
Великі списки потребують стабільних key, ефективних рядків і, за потреби, віртуалізації.
useDeferredValue і useTransition допомагають керувати пріоритетом оновлень, але не усувають вартість повільних операцій.
Мережеву продуктивність покращують правильний обсяг даних, паралельні незалежні запити та скасування застарілих запитів.
Code splitting зменшує обсяг JavaScript під час першого завантаження.
Остаточний критерій оптимізації — швидший реальний сценарій для користувача, а не більша кількість оптимізацій у коді.