Головна / Статті / Продуктивність фронтенду: від сліпих зон у перевірці коду до показників продукту

Продуктивність фронтенду: від сліпих зон у перевірці коду до показників продукту

Дізнайтеся, чому самого лише проходження перевірки коду недостатньо, які саме Core Web Vitals мають значення, та як вимірювати та усувати проблеми з продуктивністю React у реальних умовах.

2316 слів

Він пройшов перевірку коду

Ваш pull request у порядку. Логіка справна. Усі тести пройшли успішно. Його схвалив старший інженер.

Ви випускаєте зміни у четвер після обіду.

У п’ятницю вранці надходить повідомлення від менеджера продукту: «Люди кажуть, що додаток працює повільно».

Тож ви запускаєте Chrome DevTools. На вашому MacBook Pro, підключеному до домашньої оптичної мережі, з Chrome без десятка додатків, усе виглядає плавно.

Але у вашіх користувачів інша конфігурація.

Багато з них використовують пристрої Android середнього класу, випущені кілька років тому, з підключенням 4G, яке часто переходить у 3G. Вони можуть знаходитися в Джакарті, Лагосі чи Баку — місцях, де сама затримка мережі може додавати від 200 до 400 мілісекунд до кожного запиту.

Ваш додаток змушує їх сидіти та чекати.

Чому існує ця різниця

Більшість розробників фронтенду працюють у майже ідеальних умовах, а потім випускають продукт у значно більш хаотичних. Саме ця різниця є причиною виникнення проблем з продуктивністю.

Ось що ваша повсякденна середовище розробки приховує від вас:

Обмеження продуктивності CPU. У вашому робочому комп’ютері достатньо обчислювальних ресурсів. Інструменти Chrome DevTools дозволяють імітувати уповільнення CPU в 4 або 6 разів, але майже ніхто не турбується про його активацію.

Умови мережі. Тестування на localhost означає відсутність затримок. Насправді користувачі стикаються з часом передачі даних від 100 до 500 мілісекунд. Операція отримання даних, яка здається миттєвою на вашому комп’ютері, може зупинити інтерфейс на цілу секунду після його запуску.

Розмір пакету. Додавання бібліотеки під час програмування здається безкоштовним — немає видимих витрат. Однак у продакшені та сама залежність може додати ще 80 КБ до початкового пакету, який користувач із 3G мусить завантажити, перш ніж щось буде відображено.

Час парсингу JavaScript. Передача пакету на пристрій — це лише перший крок. Браузер потім мусить його проаналізувати та запустити. На слабкішому обладнанні сам процес парсингу пакета розміром 500 КБ може зайняти від 3 до 4 секунд.

Разом усі ці фактори створюють справжню розбіжність між тим, як ваш додаток здається вам, та тим, як він сприймається людьми, які ним фактично користуються — і ця розбіжність зазвичай залишається непомітною, доки скарги не змушують її стати очевидною.

Чому продуктивність — це рішення, пов’язане з продуктом

Інженери фронтенду часто відносять питання продуктивності до „технічних деталей“. Менеджери продуктів зазвичай ігнорують їх повністю, поки ситуація не перетворюється на надзвичайну.

Жоден з цих підходів не є ефективним.

Питання продуктивності має бути частиною обговорень щодо продукту, адже саме вона впливає на результати, які компанія фактично відстежує.

Дохід. Amazon повідомляє, що кожні додаткові 100 мілісекунд затримки коштують їм приблизно 1% у продажах. При доході в мільярд доларів на день це означає 10 мільйонів доларів за кожні 100 мс. У менших компаніях абсолютні цифри є меншими, але закономірність залишається тією самою.

Утримання клієнтів. Більше половини відвідувачів мобільних пристроїв — 53% — покидають сторінку, яка завантажується більше ніж за 3 секунди. Вони рідко подають скарги; вони просто йдуть та більше не повертаються.

SEO. З 2021 року Google включив показники Core Web Vitals до свого алгоритму ранжування. Повільна робота сайту призводить до його опускання в списку результатів, що означає, що все менше людей його знаходить.

Доступність. Швидкість також є питанням рівності. Люди, які використовують старі пристрої та повільні з’єднання, у нерівних пропорціях проживають у країнах, що розвиваються, та серед груп з низьким доходом. Додаток, який працює повільно, фактично виключає частину вашої аудиторії.

Як тільки ви сфокусуєте увагу на доходах, утриманні користувачів, видимості в пошукових системах та доступності, ефективність перестає здаватися необов’язковою та починає розглядатися як базова вимога.

Показники, які справді мають значення

Ви не можете виправити те, що не вимірюєте, і не можете виміряти те, що не визначили. Саме тут стає необхідним спільний словарник термінів, пов’язаних з ефективністю.

Core Web Vitals від Google наразі є найнадійнішою системою для цього.

LCP — Найбільша частка вмісту, що намальована

Цей показник відстежує час, необхідний для відображення найбільшого видимого елемента на сторінці — по суті, момент, коли користувач відчуває, що сторінка „завантажилася“.

Хорошо: менше 2,5 секунд. Треба покращити: від 2,5 до 4 секунд. Погано: більше 4 секунд.

Типові причини: занадто великі та неоптимізовані зображення, ресурси, які заважають відображенню, та повільна відповідь сервера.

INP — Час від інтеракції до наступного відображення

Цей показник вимірює затримку між дією користувача — кліком, натисканням або натисканням клавіші — та моментом візуальної реакції екрана. У 2024 році він замінив FID (First Input Delay) як стандартний показник інтерактивності.

Хорошо: менше 200 мс. Треба покращити: від 200 до 500 мс. Погано: більше 500 мс.

Типові причини: важкі обчислення на основному потоці та синхронна робота, яка блокує відображення.

CLS — Kumulативний зсув макету

Цей показник відображає, наскільки зміщується контент під час завантаження сторінки. Оцінки йдуть від 0 (жодного зсуву) вгору, причому значення понад 1 вважаються серйозними.

Хорошо: менше 0,1. Треба покращити: від 0,1 до 0,25. Погано: більше 0,25.

Типові причини: відсутність чітко вказаних значень ширини та висоти у зображеннях, динамічне додавання контенту після завантаження та веб-шрифти, які завантажуються пізно.

Як вимірювати: ваш інструментарій

Lighthouse (почніть тут)

Відкрийте Chrome DevTools, перейдіть на вкладку Lighthouse та проведіть аудит мобільного профілю з увімкненим обмеженням пропускної здатності.

Lighthouse повертає оцінку від 0 до 100 за показниками продуктивності, доступності, SEO та найкращих практик. Те, що робить його справді корисним, — це те, що він пояснює чому отримано таку оцінку та вказує, що потрібно виправити спочатку.

# Or run it from the CLI for CI/CD integration
npm install -g lighthouse
lighthouse https://yourapp.com --output html --output-path report.html

Важливо: щоразу запускайте Lighthouse у режимі інкогніто. Встановлені розширення браузера можуть спотворити результати.

React DevTools Profiler

Це, мабуть, найменш використовуваний інструмент серед тих, що є у розпорядженні розробників React, попри те, що він є одним із найпоказовіших.

Щоб скористатися ним, відкрийте React DevTools, перейдіть на вкладку Profiler, натисніть Record, взаємодійте з вашим додатком, а потім зупиніть запис.

Результатом є графік полум’я, який показує кожну обробку — які компоненти були активовані, що їх спричинило та скільки часу зайняла кожна з них.

What to look for:
- Components rendering more than they should
- Renders triggered by unrelated state changes
- Expensive components re-rendering on every keystroke

Бібліотека Web Vitals

Якщо ви хочете оцінити продуктивність вашого додатку для справжніх відвідувачів, а не лише під час локальної роботи з DevTools, бібліотека web-vitals — це ідеальний варіант:

import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(metric => {
  // Send to your analytics service
  console.log('LCP:', metric.value);
});onINP(metric => {
  console.log('INP:', metric.value);
});onCLS(metric => {
  console.log('CLS:', metric.value);
});

Цей підхід забезпечує дані, зібрані під час справжніх сеансів користувачів, а не числа, отримані в штучних лабораторних умовах.

Поновна обробка, про яку ви не здогадувалися

Один із найпідступніших проблем з продуктивністю React не проявляється очевидно. Зазвичай це виглядає так:

// ❌ Problem: selecting the full user object
function Header() {
  const user = useSelector(state => state.user);
  return <div>{user.name}</div>;
}

Цей компонент буде перероблятися щоразу, коли змінюється будь-яка властивість усередині state.user, незалежно від того, чи справді компонент читає цю властивість. Якщо об’єкт користувача містить двадцять полів, а п’ять з них часто змінюються, Header буде перероблятися у п’ять разів більше, ніж це необхідно.

// ✅ Fix: select only what you need
function Header() {
  const name = useSelector(state => state.user.name);
  return <div>{name}</div>;
}

Завдяки цій зміні Header реагує лише на зміни у name. Це зміна одним рядком, але її вплив на продуктивність реальний.

Та сама логіка застосовується до Context:

// ❌ Problem: consuming the full context
function ThemeButton() {
  const { theme, user, notifications } = useAppContext();
  return <button className={theme}>Click</button>;
}
// ✅ Fix: split contexts by update frequency
const ThemeContext = createContext();
const UserContext = createContext();function ThemeButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click</button>;
}

Lazy Loading: Припиніть надсилати код, який користувачам не потрібен

Частою помилкою у проектах на React є надсилання всього пакету додатку при першому завантаженні сторінки — включаючи код для маршрутів, які відвідувач ще не відкривав і, можливо, ніколи не відкриє.

// ❌ Problem: all routes loaded upfront
import CheckoutPage from './pages/CheckoutPage';
import AdminDashboard from './pages/AdminDashboard';
import SettingsPage from './pages/SettingsPage';
function App() {
  return (
    <Routes>
      <Route path="/checkout" element={<CheckoutPage />} />
      <Route path="/admin" element={<AdminDashboard />} />
      <Route path="/settings" element={<SettingsPage />} />
    </Routes>
  );
}
// ✅ Fix: lazy load each route
import { lazy, Suspense } from 'react';
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/checkout" element={<CheckoutPage />} />
        <Route path="/admin" element={<AdminDashboard />} />
        <Route path="/settings" element={<SettingsPage />} />
      </Routes>
    </Suspense>
  );
}

Завдяки такій налаштовці кожен маршрут стає окремим блоком, тому користувачі завантажують лише код, необхідний для сторінки, яку вони насправді переглядають. У більших додатках сама ця зміна може зменшити розмір початкового пакету на 40–60 відсотків.

Коли НЕ оптимізувати: пастка useMemo

Більшість посібників з оптимізації продуктивності пропускають цей аспект: надмірна оптимізація на ранніх етапах може негативно вплинути на ваш код.

useMemo та useCallback мають свої витрати — виділення пам’яті та порівняння залежностей при кожному оновленні. Якщо використовувати їх не належним чином, швидкість може стати навіть гіршою, ніж раніше.

// ❌ Unnecessary — this calculation is not expensive
function UserCard({ user }) {
  const displayName = useMemo(
    () => `${user.firstName} ${user.lastName}`,
    [user.firstName, user.lastName]
  );
  return <div>{displayName}</div>;
}
// ✅ Just compute it — string concatenation is instant
function UserCard({ user }) {
  const displayName = `${user.firstName} ${user.lastName}`;
  return <div>{displayName}</div>;
}
// ✅ useMemo IS worth it here — genuinely expensive calculation
function DataGrid({ rows, filters }) {
  const filteredRows = useMemo(
    () => rows.filter(row => matchesAllFilters(row, filters)),
    [rows, filters]
  );
  return <Table rows={filteredRows} />;
}

Керівний принцип: спочатку проведіть аналіз, перш ніж щось змінювати, оптимізуйте лише після цього, а потім знову перевірте результати, щоб підтвердити ефективність змін. Не покладайтеся на інтуїцію.

Якщо інструмент профілювання ніколи не позначає компонент як вузьке місце, залиште його без кешування — додана складність не варта зусиль.

Оптимізація зображень: найпростіші рішення

Зображення часто є основною причиною повільного завантаження сторінок, проте вони також належать до найпростіших проблем для вирішення.

// ❌ Unoptimized: full-size image, no lazy loading
<img src="/hero-image.png" />
// ✅ Optimized: modern format, explicit dimensions, lazy loading
<img
  src="/hero-image.webp"
  width={1200}
  height={600}
  loading="lazy"
  decoding="async"
  alt="Hero image"
/>

Кілька звичок можуть суттєво покращити ситуацію:

Замініть формати PNG або JPEG на WebP чи AVIF. WebP зазвичай зменшує розмір файлу на 25–35 відсотків порівняно з JPEG при подібній якості. AVIF стискає ще більше, хоча підтримка цього формату в браузерах ще не є універсальною.

Завжди вказуйте атрибути ширини та висоти явно. Це запобігає змінам макету після завершення завантаження зображення, що безпосередньо покращує ваш рейтинг CLS.

Застосуйте loading="lazy" до всього, що знаходиться під вікном перегляду. Тоді браузер буде чекати з завантаженням цього зображення до моменту, коли користувач збирається прокрутити його у видиму зону.

Додайте decoding="async" до зображень, які не є критичними для першого відображення. Це дозволяє браузеру декодувати зображення окремо від основного потоку, замість того щоб це блокувало процес відображення.

Справжні компроміси

Жодна техніка покращення продуктивності не є безкоштовною. Ось чесний огляд того, що ви жертвуєте:

Розділення коду за маршрутами зменшує розмір початкового пакету, але спричиняє невелику затримку під час першого переходу користувача на новий маршрут. Завантаження зображень за принципом лейзі-лоадінгу прискорює початкове завантаження, але змушує зображення з’являтися під час прокручування сторінки. Обгортання складних обчислень у useMemo зменшує кількість переробок, але призводить до збільшення складності коду та погіршення його читабельності. Кешування через Service Worker робить повторні візити майже миттєвими, але створює складну логіку скасування кешу. Рендеринг з боку сервера чи статична генерація забезпечують швидке відображення контенту та кращий SEO, але вимагають більшої інфраструктури сервера та ускладнюють процес ініціалізації коду.

Основний принцип усього цього: ніколи не оптимізуйте щось під показник, який ви насправді не вимірювали.

Оцінка Lighthouse у 95 нічого не говорить про те, чи мають справжні користувачі гарний досвід використання. Озбройте свій додаток бібліотекою Web Vitals, збирайте дані від реальних відвідувачів, визначайте справжню проблему, виправляйте саме її та потім знову перевіряйте, щоб підтвердити ефективність змін.

Практичний чек-лист

Перш ніж випустити будь-яку значущу функцію, пройдіться цим списком:

Performance Checklist
─────────────────────
□ Run Lighthouse on mobile with throttling (target score: 90+)
□ Check bundle size with webpack-bundle-analyzer or source-map-explorer
□ Verify all routes are lazy loaded
□ Confirm images use WebP/AVIF with explicit dimensions
□ Profile with React DevTools — no unnecessary re-renders
□ Check Core Web Vitals in production with web-vitals library
□ Test on a real mobile device, not just DevTools emulation
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Or for Vite
npm install --save-dev rollup-plugin-visualize

Висновок

Робота над продуктивністю — це не тимчасові заходи, які ви робите перед запуском, і не додатковий елемент, який додається після того, як функція вже працює. Це система — сукупність звичок та інструментів, вбудованих у ваш щоденний робочий процес.

Інженери, які створюють найшвидші додатки, не є за своєю природою талановитішими за тих, хто створює повільні. Вони просто більш послідовно вимірюють результати. Вони знають, який інструмент підходить для кожної проблеми, та усвідомлюють простий алгоритм: спочатку провести аналіз, виправити лише те, на що вказують дані, а потім знову перевірити, чи це допомогло.

Додаток може легко пройти всі перевірки коду, але все одно розчарувати справжніх користувачів.

Вимірюйте. Аналізуйте. Виправляйте те, що справді має значення.

Пов’язана література

  • Як уникнути проблем зі станом системи, спричинених змінами посилань у JavaScript — Дізнайтеся, чому зміна об’єктів та масивів через посилання порушує процес переробки даних у React, чому функція spread копіює дані лише поверхнево, та як безпечно створювати глибокі клони стану.
  • Як розвивалося інжиніринг фронтенду від форматування до систем масштабування — Розглядається перехід від базових технологій HTML/CSS/JS до архітектури компонентів, кешування, монорепозиторіїв та інструментів для моніторингу, необхідних для надійної роботи з мільйонами користувачів.