# Core Web Vitals для React-розробника: LCP, INP і CLS без SEO-магії

> Що насправді вимірюють LCP, INP і CLS, чим лабораторні цифри відрізняються від польових, і які саме React/Next.js-причини псують кожну метрику – з планом вимірювання у production.

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

---

**Коротка відповідь.** Core Web Vitals – це три польові метрики Google: LCP ≤ 2.5 с (швидкість появи головного контенту), INP ≤ 200 мс (відгук на взаємодії), CLS ≤ 0.1 (стабільність верстки). Рахуються вони на **75-му перцентилі реальних користувачів**, окремо для mobile і desktop – тому зелений Lighthouse на твоєму MacBook нічого не гарантує. Для React-розробника кожна метрика має свої типові причини: LCP псують зображення і шрифти, INP – довгі таски й важкі обробники, CLS – контент, що доїжджає пізніше за верстку. Нижче – розбір кожної і чесний спосіб вимірювати.

## Lab vs field: чому цифри не сходяться

Лабораторні дані (Lighthouse, локальний DevTools) – це синтетичний прогін у контрольованих умовах: одне завантаження, емульований телефон, без людини. Польові дані (CrUX, власний збір) – те, що пережили реальні користувачі за 28 днів.

Розходяться вони постійно, і найяскравіший приклад – INP: [web.dev прямо пише](https://web.dev/articles/vitals), що інструменти на кшталт Lighthouse **фізично не можуть виміряти INP**, бо в синтетичному прогоні немає взаємодій – замість нього показують proxy-метрику TBT. Тому порядок роботи завжди такий: поле каже, ЩО болить, лабораторія допомагає зрозуміти, ЧОМУ.

Оцінка проходження проста: усі три метрики в «зеленій» зоні на 75-му перцентилі. Тобто якщо у 26% користувачів LCP гірший за 2.5 с – метрика червона, навіть якщо медіана прекрасна.

## LCP: головний контент за 2.5 секунди

LCP фіксує момент, коли відрендерився найбільший елемент у viewport – зазвичай hero-зображення або заголовок. Типові React/Next-причини повільного LCP, у порядку частоти:

1. **Hero-зображення без пріоритету.** Браузер дізнається про нього пізно (після JS), або воно не оптимізоване. У Next.js: `<Image priority />` для LCP-елемента, сучасні формати, коректний `sizes`.
2. **Шрифти.** Текст-LCP чекає на webfont → невидимий текст. `next/font` із `display: swap` знімає більшу частину болю.
3. **Ланцюжок client-side запитів.** Сторінка рендериться, потім `useEffect` → fetch → ще fetch, і лише тоді з'являється контент. Це архітектурна проблема: дані для першого екрана мають їхати з сервером (SSR/RSC), а не докуповуватись на клієнті.
4. **Повільний сервер (TTFB).** Жодна фронтенд-оптимізація не врятує, якщо HTML їде 2 секунди – дивись кешування і регіони (для Next це тема [ментальної моделі навігацій](/blog/nextjs-instant-navigations)).

## INP: відгук за 200 мс

INP вимірює затримку між взаємодією (клік, тап, клавіша) і наступним кадром після неї – по всій сесії, береться найгірша. Замінила FID у 2024-му і куди жорсткіша, бо ловить не лише перший клік.

React-специфічні вбивці INP:

- **Довгі таски на main thread.** Великий ре-рендер на кожен клік = сотні мс без кадрів. Ліки: правильна структура стану і мемоізація там, де вона виправдана – я детально розбирав це в [статті про оптимізацію React](/blog/optymizatsiia-react-performance), а [React Compiler](/blog/react-compiler-usememo-usecallback) частину цієї роботи тепер забирає на себе.
- **Синхронна робота в обробниках.** Фільтрація тисячі елементів прямо в `onChange` – класика. `useTransition`/`useDeferredValue` існують саме для цього: терміновий апдейт (введений символ) окремо, важкий (перерахунок списку) – у фоні.
- **Hydration великих сторінок.** Поки React гідрує мегабайти компонентів, кліки чекають. Server Components і менші клієнтські острови – системна відповідь.

## CLS: верстка не стрибає

CLS сумує несподівані зсуви верстки. «Несподівані» – ключове слово: зсув після кліку користувача не рахується, зсув від картинки, що доїхала, – рахується.

Джерела в React-застосунках: зображення без розмірів (`next/image` вирішує, бо резервує місце), банери/alert-и, що вставляються зверху після завантаження даних, шрифт із іншими метриками (fallback-текст стрибає), скелетони, які не збігаються за розміром із реальним контентом. Правило одне: **усе, що з'явиться пізніше, має мати зарезервоване місце вже зараз.**

## Як міряти по-чесному: web-vitals у production

Бібліотека `web-vitals` – офіційний спосіб зібрати ті самі значення, які бачить Google. Мінімальна інтеграція в наш стек (PostHog уже є):

```ts
// app/web-vitals.ts – викликати з клієнтського компонента в layout
import { onLCP, onINP, onCLS, type Metric } from 'web-vitals';
import posthog from 'posthog-js';

function report(metric: Metric) {
  posthog.capture('web_vitals', {
    metric_name: metric.name,       // 'LCP' | 'INP' | 'CLS'
    metric_value: metric.value,     // мс або безрозмірний CLS
    metric_rating: metric.rating,   // 'good' | 'needs-improvement' | 'poor'
    metric_id: metric.id,           // дедуплікація в аналітиці
  });
}

export function initWebVitals() {
  onLCP(report);
  onINP(report);
  onCLS(report);
}
```

Далі в аналітиці дивишся 75-й перцентиль по `metric_value` з розбивкою mobile/desktop – і отримуєш власні польові дані швидше, ніж CrUX збере свої 28 днів. Безкоштовна альтернатива для старту – PageSpeed Insights, який показує CrUX-дані твого домену, якщо трафіку достатньо.

## Пріоритизація: лагодь за впливом, не за списком

Антипатерн – «зробимо всі пункти з аудиту». Нормальний процес: подивитись, яка метрика червона в полі → знайти сторінки з найбільшим трафіком серед проблемних → полагодити одну причину → переміряти. Оптимізація LCP на сторінці, куди заходить 50 людей на місяць, – це хобі, а не перформанс-робота.

І окремо про чесність: я свідомо не наводжу тут «до/після: було 4.1 с, стало 1.9 с» із вигаданого проєкту. Числа мають сенс лише твої власні – методика вище дає їх за тиждень збору.

## Типові помилки

- **Оптимізувати під Lighthouse-бал**, а не під польові метрики. Бал – проксі, ранжування і користувачі живуть у полі.
- **Міряти лише на десктопі.** Поріг один, а 75-й перцентиль mobile зазвичай удвічі гірший.
- **Лікувати INP через `useMemo` на все.** Спершу профіль: може, проблема в одному обробнику, а не в рендерингу взагалі.
- **Скелетони заради скелетонів**, які самі створюють CLS, бо не збігаються з контентом за висотою.
- **Разове вимірювання.** Vitals – це моніторинг, не аудит раз на квартал: кожен реліз може зрушити метрики.

## Практичне завдання

Підключи `web-vitals` до свого проєкту (20 хвилин разом з аналітикою), збери тиждень даних і побудуй три числа: 75-й перцентиль LCP, INP, CLS на mobile. Потім відкрий найповільнішу за LCP сторінку в DevTools → Performance, запиши trace завантаження і знайди LCP-елемент у таймлайні: що блокувало його появу – сервер, зображення, шрифт чи JS? Одна знайдена причина з реальних даних вартує більше за будь-який чекліст.
