Seo

Core Web Vitals: як виміряти і покращити

LCP, INP, CLS — пороги, інструменти вимірювання (PageSpeed Insights, CrUX, Lighthouse, web-vitals JS) і конкретні способи покращити кожну метрику.

Автор: Андрій Коваленко 10 хв читання
Зміст

Core Web Vitals — три метрики Google: LCP, INP і CLS. Вони вимірюють, наскільки швидко і стабільно завантажується сторінка для реального користувача. З 2021 року це офіційний ranking-фактор. У цій статті — що означає кожна метрика, де їх виміряти і конкретні дії для покращення.

Повний технічний контекст CWV у системі технічного SEO — у посібнику Технічне SEO: з чого почати.

Три метрики: пороги і що вони вимірюють

МетрикаЩо вимірюєЗелена зонаЖовтаЧервона
LCP — Largest Contentful PaintЧас до відмалювання найбільшого елемента<2.5 с2.5–4 с>4 с
INP — Interaction to Next PaintЗатримка відгуку на будь-яку взаємодію<200 мс200–500 мс>500 мс
CLS — Cumulative Layout ShiftСума зсувів елементів під час завантаження<0.10.1–0.25>0.25

Google приймає рішення на рівні URL: кожна сторінка оцінюється окремо. Для ranking-сигналу Google дивиться на 75-й перцентиль реальних користувачів за останні 28 днів (CrUX-дані).

INP замінив FID 12 березня 2024

До березня 2024 третьою метрикою CWV був FID (First Input Delay). FID вимірював тільки першу взаємодію. INP суворіший: фіксує всі кліки, тапи і натискання клавіш протягом сесії і бере 98-й перцентиль. Якщо ваш сайт раніше мав хороший FID, але ваш JS-код важкий — INP може показати червону зону навіть там, де FID був зеленим.

Де виміряти Core Web Vitals

PageSpeed Insights — стартова точка

Адреса: pagespeed.web.dev. Введіть будь-який URL — отримаєте два блоки:

  1. Field data (реальні дані) — з CrUX за останні 28 днів. Це і є те, що Google використовує для ranking. Якщо сторінка отримує менше ~1000 візитів/місяць — CrUX-даних немає, з’явиться повідомлення «Insufficient data».
  2. Lab data (лабораторні дані) — Lighthouse у симульованих умовах (емульований Moto G Power, повільний 4G). Використовуйте для діагностики і локального налагодження.

Різниця між field і lab може бути значною: реальні користувачі мають різні пристрої, мережі, кеш браузера. Ціль — зелена зона у field data, але якщо її немає — спочатку виправте lab data.

Google Search Console — масштаб по всьому сайту

GSC → Core Web Vitals (бічна панель) показує URL-групи зі статусами Poor, Needs Improvement і Good для мобайлу і десктопу окремо. Тут видно, які шаблони сторінок мають найбільше проблем: наприклад, всі сторінки категорій у червоній зоні, а home page у зеленій.

GSC також пропонує кнопку «Validate Fix» після виправлення — Google перевіряє сторінки з групи і оновлює статус протягом кількох тижнів.

Chrome DevTools і Lighthouse

В Chrome: F12 → вкладка Lighthouse → запуск Performance аудиту. Корисно для:

  • Знайти конкретний LCP-елемент.
  • Побачити «Opportunities» і «Diagnostics» — пояснення чому метрика погана.
  • Порівняти до/після внесення правок локально.

У вкладці Performance після запису завантаження: у треку Timings є мітки LCP, FCP, LCP з посиланням на DOM-вузол.

web-vitals.js — вимірювання в production

Бібліотека від Google для збору реальних CWV-даних із вашого сайту:

<script type="module">
  import {onLCP, onINP, onCLS} from 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.js';

  function sendToAnalytics(metric) {
    // Відправляємо в GA4 як custom event
    gtag('event', metric.name, {
      value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
      metric_id: metric.id,
      metric_value: metric.value,
      metric_delta: metric.delta,
      metric_rating: metric.rating,  // 'good' / 'needs-improvement' / 'poor'
    });
  }

  onLCP(sendToAnalytics);
  onINP(sendToAnalytics);
  onCLS(sendToAnalytics);
</script>

Пакет web-vitals також встановлюється через npm: npm install web-vitals. Attribution-версія додає деталі: який елемент спричинив LCP, яка взаємодія дала найгірший INP, які елементи зсунулися при CLS.

CrUX API і дашборд

Chrome UX Report (CrUX) — публічний датасет Google. Доступ:

  • CrUX API (безкоштовно, 150 запитів за хвилину): https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=API_KEY — дані по конкретному URL або домену.
  • CrUX дашборд: lookerstudio.google.com → шукайте «CrUX Dashboard» — готовий Looker Studio звіт, достатньо ввести домен.
  • BigQuery: повний датасет, оновлюється щомісяця.

CrUX агрегує дані за 28 днів ковзним вікном і оновлюється щодня для CrUX API (щомісяця для BigQuery).

Lab vs Field: коли що використовувати

СитуаціяЯке джерело
Перевірити, чи сайт ранжуватиметьсяField data (CrUX, GSC)
Знайти причину повільного LCPLab data (Lighthouse, PSI diagnostics)
Порівняти до/після правки локальноLab data (DevTools, Lighthouse CI)
Моніторинг реальних користувачівweb-vitals.js → GA4 або інший колектор
Масштаб проблем по сайтуGSC Core Web Vitals report

LCP: як покращити

LCP — час до відмалювання найбільшого елемента above the fold. У 80%+ випадків це hero-зображення або великий заголовок h1.

Крок 1: знайти LCP-елемент

PageSpeed Insights → розділ «Largest Contentful Paint element» покаже конкретний DOM-вузол. Якщо це <img> — дивіться на оптимізацію зображень. Якщо <h1> — проблема у шрифтах або server response time.

Крок 2: оптимізувати зображення

<!-- Погано: немає розмірів, немає WebP, немає preload -->
<img src="hero.jpg" alt="Hero">

<!-- Добре: WebP, srcset, правильні розміри, preload для LCP-зображення -->
<link rel="preload" as="image" href="hero-800w.webp"
      imagesrcset="hero-400w.webp 400w, hero-800w.webp 800w"
      imagesizes="(max-width: 640px) 400px, 800px">

<img src="hero-800w.webp"
     srcset="hero-400w.webp 400w, hero-800w.webp 800w"
     sizes="(max-width: 640px) 400px, 800px"
     width="800" height="450"
     alt="Hero"
     fetchpriority="high">

Ключові моменти:

  • WebP або AVIF замість JPEG/PNG. WebP: -25-35% від JPEG при тій самій якості. AVIF: ще -20% від WebP, але підтримка браузерів дещо менша.
  • fetchpriority="high" для LCP-зображення — каже браузеру підвантажити першим.
  • <link rel="preload"> для LCP-зображення в <head> — браузер починає завантаження до того, як парсить img-тег.
  • loading="lazy" для зображень нижче першого екрану — не ставте на LCP-елемент.

Крок 3: зменшити TTFB

Time to First Byte — час до першого байта відповіді сервера. Ціль: <800мс, ідеал <200мс.

Способи:

  • CDN (Cloudflare, Bunny.net, Fastly) — віддає HTML з edge-ноди поруч із користувачем.
  • Кешування на сервері — повноцінний page cache для WordPress (LiteSpeed Cache, W3 Total Cache), статичні генератори (Hugo, Astro) дають TTFB <50мс за замовчуванням.
  • Оптимізація бази даних — довгі SQL-запити безпосередньо збільшують TTFB.
  • Хостинг — shared hosting на overcrowded серверах дає TTFB 1-3с. VPS або managed WordPress хостинг — суттєвий стрибок.

Крок 4: Server-side rendering замість client-side

Single-page applications (React, Vue, Angular) без SSR рендерять контент у браузері — LCP не може початися, поки не завантажиться і не виконається весь JS-бандл. Рішення:

  • Next.js (React) з SSR або Static Site Generation.
  • Nuxt.js (Vue) з SSR.
  • Astro — відправляє нуль JS за замовчуванням.
  • Hugo — статичний генератор, ультрашвидкий.

Крок 5: Resource hints

<!-- DNS prefetch для зовнішніх ресурсів (шрифти, CDN) -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">

<!-- Preconnect — встановлює TCP/TLS до сервера завчасно -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Preload — завантажує ресурс з найвищим пріоритетом -->
<link rel="preload" href="/fonts/inter-v13-latin-regular.woff2" as="font" type="font/woff2" crossorigin>

INP: як покращити

INP вимірює, наскільки швидко сторінка реагує на взаємодію користувача. Більшість проблем INP — це JavaScript, що блокує main thread.

Знайти проблемну взаємодію

У web-vitals.js attribution-версія показує, яка взаємодія дала найгірший INP:

import {onINP} from 'web-vitals/attribution';

onINP(({value, attribution}) => {
  console.log('INP:', value, 'ms');
  console.log('Елемент:', attribution.interactionTarget);
  console.log('Тип:', attribution.interactionType);
  console.log('Час обробки:', attribution.processingDuration);
});

В Chrome DevTools → Performance → після кліку на сторінці: знайдіть у треку Main довгі tasks (червоні). Клацніть на task — побачите, яка функція займає час.

Зменшити long tasks

Long task — будь-яка задача на main thread довше 50мс. INP стає поганим, якщо після кліку є long task, який затримує відповідь.

// Погано: важкий обробник блокує main thread
button.addEventListener('click', () => {
  const result = heavyComputation(); // займає 300мс
  updateUI(result);
});

// Добре: розбиваємо на chunks
button.addEventListener('click', async () => {
  updateUI('Завантажую...');
  
  // Передаємо управління браузеру між ітераціями
  await scheduler.yield(); // Chrome 115+, або setTimeout(0) як fallback
  
  const result = await computeInChunks();
  updateUI(result);
});

Defer і async для скриптів

<!-- Блокує парсинг HTML — уникайте для не-критичних скриптів -->
<script src="analytics.js"></script>

<!-- async: завантажує паралельно, виконує як тільки завантажився -->
<script src="analytics.js" async></script>

<!-- defer: завантажує паралельно, виконує після парсингу HTML -->
<script src="non-critical.js" defer></script>

Google Tag Manager і більшість аналітичних скриптів — використовуйте async. Скрипти, що залежать від DOM — defer.

Web Workers для важких обчислень

Якщо потрібна важка обробка даних — виносьте в Web Worker, щоб не блокувати main thread:

// main.js
const worker = new Worker('heavy-worker.js');

button.addEventListener('click', () => {
  worker.postMessage({data: largeDataset});
});

worker.onmessage = (e) => {
  updateUI(e.data.result);
};

// heavy-worker.js
self.onmessage = (e) => {
  const result = processData(e.data.data); // не блокує UI
  self.postMessage({result});
};

React: специфічна оптимізація

Якщо використовуєте React — INP часто страждає через зайві re-renders:

// useMemo — кешувати важкі обчислення
const sortedItems = useMemo(
  () => items.sort((a, b) => b.date - a.date),
  [items]
);

// React.memo — не re-render якщо props не змінились
const ItemCard = React.memo(({item}) => <div>{item.title}</div>);

// useTransition — позначити оновлення як не-термінове
const [isPending, startTransition] = useTransition();

function handleFilter(value) {
  startTransition(() => {
    setFilteredItems(items.filter(i => i.name.includes(value)));
  });
}

CLS: як покращити

CLS вимірює, наскільки сторінка «стрибає» під час завантаження. Кожного разу, коли елемент зсувається без взаємодії користувача — це вноситься у CLS.

Завжди задавати розміри для медіа

Найпоширеніша причина CLS — зображення і відео без явних розмірів. Браузер не знає розміру, резервує 0px, і після завантаження зображення весь контент нижче зсувається.

<!-- Погано: браузер не знає розмірів -->
<img src="product.jpg" alt="Продукт">

<!-- Добре: браузер резервує місце заздалегідь -->
<img src="product.jpg" width="600" height="400" alt="Продукт">

<!-- Для responsive зображень — aspect-ratio через CSS -->
<style>
  .product-img {
    aspect-ratio: 600 / 400;
    width: 100%;
    height: auto;
  }
</style>
<img class="product-img" src="product.jpg" alt="Продукт">

Те саме для <video>:

<video width="1280" height="720" controls>
  <source src="demo.mp4" type="video/mp4">
</video>

Резервувати місце для реклами і embeds

/* Банер 728×90 — завжди резервуємо місце */
.ad-container {
  min-height: 90px;
  width: 728px;
}

/* Responsive варіант */
.ad-slot {
  min-height: 250px; /* типовий розмір medium rectangle */
}

Для динамічного контенту (чат, cookie banner, sticky notification):

  • Cookie banner — показуйте знизу сторінки, а не зверху. Banner зверху штовхає весь контент вниз = CLS.
  • Lazy-loaded контент — не вставляйте між існуючими елементами. Додавайте в кінець або в заздалегідь зарезервований простір.

Шрифти: font-display

Коли кастомний шрифт завантажується пізніше системного — текст перемальовується, що може викликати CLS:

@font-face {
  font-family: 'Inter';
  font-display: optional; /* ідеал для CLS: якщо не завантажився за ~100мс — не підміняти */
  src: url('/fonts/inter.woff2') format('woff2');
}

/* Або: swap — завжди показувати, підміняти після завантаження */
/* Це може дати невеликий CLS, але краще ніж невидимий текст (FOIT) */
font-display: swap;

Preload найважливіших шрифтів у <head> зменшує затримку:

<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>

Типові помилки і як їх уникнути

  1. Вимірюєте тільки home page. У GSC часто landing pages, статті і сторінки категорій мають гірші CWV за головну. Дивіться GSC Core Web Vitals report — він групує сторінки за шаблоном.
  2. Ставите fetchpriority="high" на всі зображення. Якщо всі «важливі» — жодне не важливіше. Один fetchpriority="high" на LCP-зображення.
  3. Lazy load на LCP-зображення. loading="lazy" на hero-зображенні — найшвидший спосіб зіпсувати LCP. Lazy load тільки для зображень нижче першого екрану.
  4. Великий JS-бандл без code splitting. Якщо весь frontend-код у одному файлі на 500кб — браузер завантажує і парсить все перед рендерингом. Webpack/Vite code splitting + dynamic imports.
  5. Рекламний банер без зарезервованого місця. AdSense і programmatic реклама часто змінюють розмір після завантаження — CLS стрибає. Фіксуйте min-height контейнера.
  6. Ігноруєте mobile дані. Більшість сайтів мають гірші мобільні CWV. Емуляція в Lighthouse не передає реальний досвід — використовуйте field data.
  7. Не чекаєте оновлення CrUX. CrUX оновлюється поступово (28-денне вікно). Виправили CLS — GSC покаже покращення через 4-6 тижнів, не завтра.
  8. GTM без оптимізації тегів. Google Tag Manager не блокує LCP (асинхронний), але важкі теги (рекламні SDK, heatmap, live chat) створюють long tasks → поганий INP. Аудит активних тегів GTM — стандартна CWV-оптимізація.

Практична черговість: що виправляти першим

Якщо всі метрики червоні — рекомендую таку черговість:

ПріоритетДіяЩо фіксуєСкладність
1Додати width/height до зображеньCLSНизька — 1-2 год
2WebP/AVIF + fetchpriority="high" на LCPLCPНизька
3<link rel="preload"> для LCP-image і шрифтівLCPНизька
4CDN або page cacheLCP (TTFB)Середня
5Cookie banner знизу, не зверхуCLSНизька
6defer/async для не-критичних скриптівINP (TBT)Низька
7Аудит і очищення GTM-тегівINPСередня
8Code splitting JS-бандлуINPВисока
9SSR/SSG замість клієнтського рендерингуLCP + INPВисока

Моніторинг після виправлень

Разове виправлення — недостатньо. CWV може погіршитися після кожного релізу. Збудуйте процес:

  1. Lighthouse CI у CI/CD pipeline — автоматично перевіряє CWV на кожному PR перед деплоєм.
  2. web-vitals.js → GA4 — збирайте реальні дані постійно, налаштуйте алерти на погіршення.
  3. GSC Core Web Vitals report — перевіряйте раз на тиждень після великих релізів.
  4. PageSpeed Insights API — автоматичні щотижневі знімки для ключових сторінок.

Приклад щотижневого моніторингу через PSI API (bash):

URL="https://example.com"
API_KEY="YOUR_KEY"

curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${URL}&strategy=mobile&key=${API_KEY}" \
  | jq '.loadingExperience.metrics | {
      LCP: .LARGEST_CONTENTFUL_PAINT_MS.percentile,
      INP: .INTERACTION_TO_NEXT_PAINT.percentile,
      CLS: .CUMULATIVE_LAYOUT_SHIFT_SCORE.percentile
    }'

Пов’язані ресурси

Посібники:

Чек-листи:

Інструменти:

  • Meta Tag Checker — перевірити title, description, canonical одним запитом
  • SERP Preview — попередній перегляд сніпету
  • Всі інструменти сайту → /tools/

Схожі статті

Дивіться також

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

Хто веде сайт і чому без AI