Core Web Vitals: як виміряти і покращити
LCP, INP, CLS — пороги, інструменти вимірювання (PageSpeed Insights, CrUX, Lighthouse, web-vitals JS) і конкретні способи покращити кожну метрику.
Зміст
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.1 | 0.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 — отримаєте два блоки:
- Field data (реальні дані) — з CrUX за останні 28 днів. Це і є те, що Google використовує для ranking. Якщо сторінка отримує менше ~1000 візитів/місяць — CrUX-даних немає, з’явиться повідомлення «Insufficient data».
- 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) |
| Знайти причину повільного LCP | Lab 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>
Типові помилки і як їх уникнути
- Вимірюєте тільки home page. У GSC часто landing pages, статті і сторінки категорій мають гірші CWV за головну. Дивіться GSC Core Web Vitals report — він групує сторінки за шаблоном.
- Ставите
fetchpriority="high"на всі зображення. Якщо всі «важливі» — жодне не важливіше. Одинfetchpriority="high"на LCP-зображення. - Lazy load на LCP-зображення.
loading="lazy"на hero-зображенні — найшвидший спосіб зіпсувати LCP. Lazy load тільки для зображень нижче першого екрану. - Великий JS-бандл без code splitting. Якщо весь frontend-код у одному файлі на 500кб — браузер завантажує і парсить все перед рендерингом. Webpack/Vite code splitting + dynamic imports.
- Рекламний банер без зарезервованого місця. AdSense і programmatic реклама часто змінюють розмір після завантаження — CLS стрибає. Фіксуйте
min-heightконтейнера. - Ігноруєте mobile дані. Більшість сайтів мають гірші мобільні CWV. Емуляція в Lighthouse не передає реальний досвід — використовуйте field data.
- Не чекаєте оновлення CrUX. CrUX оновлюється поступово (28-денне вікно). Виправили CLS — GSC покаже покращення через 4-6 тижнів, не завтра.
- GTM без оптимізації тегів. Google Tag Manager не блокує LCP (асинхронний), але важкі теги (рекламні SDK, heatmap, live chat) створюють long tasks → поганий INP. Аудит активних тегів GTM — стандартна CWV-оптимізація.
Практична черговість: що виправляти першим
Якщо всі метрики червоні — рекомендую таку черговість:
| Пріоритет | Дія | Що фіксує | Складність |
|---|---|---|---|
| 1 | Додати width/height до зображень | CLS | Низька — 1-2 год |
| 2 | WebP/AVIF + fetchpriority="high" на LCP | LCP | Низька |
| 3 | <link rel="preload"> для LCP-image і шрифтів | LCP | Низька |
| 4 | CDN або page cache | LCP (TTFB) | Середня |
| 5 | Cookie banner знизу, не зверху | CLS | Низька |
| 6 | defer/async для не-критичних скриптів | INP (TBT) | Низька |
| 7 | Аудит і очищення GTM-тегів | INP | Середня |
| 8 | Code splitting JS-бандлу | INP | Висока |
| 9 | SSR/SSG замість клієнтського рендерингу | LCP + INP | Висока |
Моніторинг після виправлень
Разове виправлення — недостатньо. CWV може погіршитися після кожного релізу. Збудуйте процес:
- Lighthouse CI у CI/CD pipeline — автоматично перевіряє CWV на кожному PR перед деплоєм.
- web-vitals.js → GA4 — збирайте реальні дані постійно, налаштуйте алерти на погіршення.
- GSC Core Web Vitals report — перевіряйте раз на тиждень після великих релізів.
- 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
}'
Пов’язані ресурси
Посібники:
- Технічне SEO: з чого почати — повний огляд технічного SEO, crawling, indexing, hreflang, schema
Чек-листи:
- SEO Audit Checklist — пошаговий аудит включаючи CWV
Інструменти:
- Meta Tag Checker — перевірити title, description, canonical одним запитом
- SERP Preview — попередній перегляд сніпету
- Всі інструменти сайту → /tools/
Схожі статті
Технічне SEO: повний посібник з чого почати — індексація, Core Web Vitals, schema.org, hreflang
Покроковий посібник з технічного SEO для початківців: crawling, indexing, robots.txt, sitemap.xml, canonical, hreflang, Core Web Vitals (LCP/INP/CLS), schema.org, mobile-first і типові помилки.
Seorobots.txt і sitemap.xml: повне налаштування
Синтаксис robots.txt, що не можна блокувати, AI-бот директиви, структура sitemap.xml, відправка в Google Search Console і типові помилки — практичне налаштування.
ToolsRobots.txt Tester — перевірка правил сканування
Безкоштовний онлайн-тестер robots.txt: перевіряє дозвіл на сканування URL для 16 пошукових і AI-ботів (Googlebot, Bingbot, GPTBot, ClaudeBot), показує правило і рядок.