Починайте з польових даних, а не з оцінки Lighthouse. У Search Console або PageSpeed Insights знайдіть метрику, яка не проходить на 75-му перцентилі, і тип пристроїв, де це відбувається, а потім підключіть attribution-збірку web-vitals, щоб реальні сесії показали винний елемент, взаємодію або зсув. LCP виправляйте за найбільшою складовою, INP за довгим завданням за повільними взаємодіями, CLS резервуванням місця для всього, що рухається. Лабораторні інструменти відтворюють проблему й перевіряють виправлення, але не вирішують, чи ви проходите оцінку.
Три метрики та їхні пороги
Огляд Web Vitals на web.dev називає три стабільні метрики Core Web Vitals:
- Largest Contentful Paint (LCP), завантаження: добре при 2,5 секунди й менше, погано понад 4 секунди.
- Interaction to Next Paint (INP), чутливість: добре при 200 мілісекундах і менше, погано понад 500 мілісекунд.
- Cumulative Layout Shift (CLS), візуальна стабільність: добре при 0,1 й менше, погано понад 0,25.
Значення між межами отримують оцінку «потребує покращення». Пороги застосовують до 75-го перцентиля завантажень сторінок окремо для мобільних і десктопних пристроїв, і сторінка проходить оцінку лише тоді, коли всі три метрики добрі на цьому перцентилі. Пристойна медіана може приховувати чверть візитів із повільних телефонів, а оцінка існує саме для того, щоб їх помітити.
INP замінив First Input Delay у 2024 році. Він відстежує кліки, дотики й натискання клавіш упродовж усього життя сторінки та вимірює кожну взаємодію до відмальовування наступного кадру. Наведення, масштабування й прокручування не враховуються, а на кожні 50 взаємодій відкидається одна найповільніша, тож поодинокий викид не визначає результат.
README web-vitals зазначає, що onLCP() і onINP() працюють у Chromium, Firefox і Safari, а onCLS() лише в Chromium. Дані сумісності MDN показують, що Safari додав API LCP та Event Timing у версії 26.2. Польовий набір даних Google досі збирається лише з користувачів Chrome.
Польові дані проти лабораторних
Польові дані надходять від реальних візитів. Chrome UX Report (CrUX) агрегує дані відповідних користувачів Chrome за ковзне вікно у 28 днів, на рівні origin і URL, для сторінок із достатнім публічним трафіком. PageSpeed Insights показує ці дані на початку звіту й переходить до даних origin, якщо для URL їх замало. Звіт Core Web Vitals у Search Console використовує те саме джерело, групує схожі URL, надає групі статус найгіршої метрики та окремо відстежує мобільні й десктопні пристрої. Виправлення, викотене сьогодні, зсуває ці цифри поступово, упродовж чотирьох тижнів.
Лабораторні дані отримують з одного контрольованого завантаження. Lighthouse завантажує сторінку на емульованих пристрої та мережі. Панель Performance у DevTools показує локальні LCP, CLS і INP у реальному часі, записує взаємодії з їхніми фазами та зсуви макета з їхніми оцінками, а також уміє підтягнути дані CrUX із рекомендованим уповільненням CPU й мережі, схожим на умови ваших користувачів.
Розбіжності передбачувані, і web.dev розбирає їх у статті про відмінності лабораторних і польових даних:
- LCP: лабораторний прогін іде з холодним кешем, одним розміром вікна й без персоналізації. У реальних користувачів ресурси можуть бути в кеші, елемент LCP може бути іншим, може трапитися варіант A/B-тесту, а відновлення з bfcache теж потрапляють у польові дані.
- INP: у навігаційного прогону Lighthouse немає взаємодій, тому замість INP він показує Total Blocking Time. TBT допомагає знайти блокування під час завантаження, але не бачить повільного кліку пізніше в сесії.
- CLS: лабораторія бачить зсуви під час завантаження. Польовий CLS охоплює все життя сторінки, зокрема ліниво завантажений вміст без розмірів і пізні рекламні блоки.
Вирішуйте, що зламано, за польовими даними, а потім відтворюйте проблему в лабораторії з реалістичним уповільненням. CrUX API одразу повертає 75-й перцентиль і оновлюється щодня:
curl -s --request POST \
"https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$CRUX_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"origin": "https://example.com",
"formFactor": "PHONE",
"metrics": [
"largest_contentful_paint",
"interaction_to_next_paint",
"cumulative_layout_shift",
"largest_contentful_paint_image_time_to_first_byte",
"largest_contentful_paint_image_resource_load_delay",
"largest_contentful_paint_image_resource_load_duration",
"largest_contentful_paint_image_element_render_delay"
]
}'
Останні чотири метрики показують складові LCP для завантажень, де елементом LCP є зображення.
Збирання даних реальних користувачів з attribution-збіркою
CrUX повідомляє, що метрика не проходить, але рідко пояснює чому. Бібліотека web-vitals вимірює метрики так само, як Chrome, а її attribution-збірка додає елемент, розбивку за часом і стан завантаження документа. Актуальна мажорна версія зараз шоста. Вона вміє повідомляти про soft navigations в односторінкових застосунках на Chromium 151 і новіших, але як CrUX їх ураховуватиме, поки не вирішено.
npm install web-vitals
Цей модуль зберігає останній запис для кожного екземпляра метрики та надсилає пакет через navigator.sendBeacon(), коли сторінка стає прихованою, як радить README:
import {onCLS, onINP, onLCP} from 'web-vitals/attribution';
const queue = new Map();
function toRecord(metric) {
const {name, value, rating, id, attribution} = metric;
const record = {name, value, rating, id, page: metric.navigationURL ?? location.href};
if (name === 'LCP') {
record.target = attribution.target;
record.ttfb = attribution.timeToFirstByte;
record.loadDelay = attribution.resourceLoadDelay;
record.loadDuration = attribution.resourceLoadDuration;
record.renderDelay = attribution.elementRenderDelay;
} else if (name === 'INP') {
record.target = attribution.interactionTarget;
record.inputDelay = attribution.inputDelay;
record.processing = attribution.processingDuration;
record.presentation = attribution.presentationDelay;
record.loadState = attribution.loadState;
record.script = attribution.longestScript?.entry.sourceURL;
} else if (name === 'CLS') {
record.target = attribution.largestShiftTarget;
record.loadState = attribution.loadState;
}
return record;
}
function flush() {
if (queue.size === 0) return;
navigator.sendBeacon('/rum', JSON.stringify([...queue.values()]));
queue.clear();
}
onLCP((metric) => queue.set(metric.id, toRecord(metric)));
onINP((metric) => queue.set(metric.id, toRecord(metric)));
onCLS((metric) => queue.set(metric.id, toRecord(metric)));
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
CLS та INP можуть надходити кілька разів, тому збирач зберігає останнє значення для кожного id. INP відсутній, якщо користувач жодного разу не взаємодіяв зі сторінкою. Агрегуйте дані за шаблоном сторінки й типом пристрою, потім за target, щоб знайти кілька елементів, які визначають p75. Ваші цифри не збігатимуться з CrUX точно: у них є інші браузери, а бібліотека не бачить вмісту iframe. На навантажених сайтах використовуйте вибірку, тримайте низьку кардинальність міток і застосовуйте звичайні правила зберігання. Посібник зі спостережуваності AI-агентів описує ту саму дисципліну роботи з телеметрією.
LCP: знайдіть повільну складову
Посібник з оптимізації LCP ділить LCP на чотири послідовні частини:
- Time to first byte (TTFB): від початку навігації до першого байта HTML.
- Resource load delay: від TTFB до початку запиту ресурсу LCP.
- Resource load duration: саме завантаження ресурсу.
- Element render delay: від готовності ресурсу до відмальовування елемента.
Для текстового елемента LCP дві середні частини дорівнюють нулю. Посібник пропонує орієнтир: приблизно по 40 відсотків на TTFB і завантаження ресурсу та менше 10 відсотків на кожну затримку. Для 2,5 секунди це близько 1 секунди, 250 мілісекунд, 1 секунди й 250 мілісекунд. Велика затримка завантаження означає пізнє виявлення ресурсу. Велика затримка відмальовування означає, що щось заважало малювати. Стиснення зображення не виправить ні того, ні іншого.
Виправлення LCP, які змінюють цифру
Time to first byte
web.dev вважає добрим TTFB до 0,8 секунди й поганим понад 1,8 секунди. До нього входять редиректи, запуск service worker, DNS, з’єднання й TLS, а також сам запит. Приберіть ланцюжки редиректів, кешуйте HTML на межі CDN для анонімного трафіку й віддавайте ассети з хешем в імені з довгим незмінним терміном кешування. Посібник із кешування статики в Nginx і Cloudflare розбирає заголовки та правила. Якщо origin працює на невеликому VPS, тримайте його мінімальним за чек-листом захисту Linux VPS і дозвольте edge приймати трафік.
curl -s -o /dev/null \
-w 'dns %{time_namelookup}\nconnect %{time_connect}\ntls %{time_appconnect}\nttfb %{time_starttransfer}\n' \
https://example.com/
curl -sI https://example.com/ | grep -iE '^(cache-control|age|cf-cache-status):'
Resource load delay
Зображення LCP має починати завантажуватися разом із першими ресурсами сторінки. Розмістіть його у вихідному HTML як <img>, ніколи не ставте йому loading="lazy" і додайте fetchpriority="high", який MDN позначає в Chromium, Firefox 132+ і Safari 17.2+. Типова причина проблеми полягає в hero-блоці, що рендериться на клієнті: зображення не можна запросити, доки бандл не завантажиться, не виконається й часто не отримає дані. Рендерте hero на сервері або заздалегідь. Зображення LCP із CSS завантажуйте через preload.
<link rel="preload" as="image" href="/img/hero-bg.avif"
type="image/avif" fetchpriority="high">
<picture>
<source type="image/avif"
srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w"
sizes="(max-width: 800px) 100vw, 800px">
<img src="/img/hero-800.jpg"
srcset="/img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
sizes="(max-width: 800px) 100vw, 800px"
width="800" height="450" alt="Product dashboard"
fetchpriority="high">
</picture>
<link> потрібен лише для зображення з CSS, а <picture> для звичайного вмісту.
Resource load duration
Надсилайте менше байтів. Віддавайте AVIF або WebP із запасним форматом, дозвольте srcset і sizes підібрати файл під реальний розмір слота та роздавайте зображення через CDN із терміном кешування, за якого повторний візит обходиться без завантаження. Тримайте мало запитів із високим пріоритетом, бо кожен із них конкурує із зображенням LCP.
Element render delay
Якщо зображення прийшло рано, а відмалювалося пізно, шукайте великі блокувальні стилі, синхронні скрипти в <head> і шрифти з font-display: block або auto. Скрипти, що приховують сторінку до свого завершення, як роблять деякі інструменти експериментів, додають свій час роботи до LCP. Вбудовуйте критичний CSS, відкладайте решту й відмальовуйте hero без клієнтського JavaScript.
INP: знайдіть взаємодію та її довге завдання
Взаємодія має три фази. Input delay це очікування до запуску обробників, зазвичай за іншим завданням. Processing duration це робота самих обробників. Presentation delay триває від кінця обробників до наступного кадру й охоплює стилі, розкладку та відмальовування.
Читайте їх разом. Великий input delay при loadState, що дорівнює dom-interactive або dom-content-loaded, означає, що користувач натиснув, поки сторінка завантажувалася, зазвичай під час гідратації або сторонніх скриптів. Великий processing вказує на обробник. Великий presentation вказує на великий DOM або примусову розкладку. У Chromium поле longestScript з Long Animation Frames API називає скрипт, що працював найдовше.
Щоб відтворити проблему, відкрийте панель Performance, увімкніть запропоноване уповільнення CPU й повторіть взаємодію з елементом із target у RUM. Будь-яке завдання довше за 50 мілісекунд вважається довгим, і взаємодія, що стала за ним у чергу, отримує його залишковий час.
Виправлення INP: менше роботи на взаємодію
Спершу відмалюйте відповідь
Зробіть мінімум, що дає видимий зворотний зв’язок, дозвольте браузеру відмалювати кадр і лише потім продовжуйте. Аналітиці й автозбереженню рідко потрібно завершитися до наступного кадру. Посібник із довгих завдань наводить такий помічник:
function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
filterButton.addEventListener('click', async () => {
filterButton.setAttribute('aria-busy', 'true');
await yieldToMain();
const rows = filterRows(allRows, currentQuery());
renderRows(rows);
filterButton.removeAttribute('aria-busy');
await yieldToMain();
sendAnalytics('filter', rows.length);
});
async function processInChunks(items, handleItem, budgetMs = 40) {
let lastYield = performance.now();
for (const item of items) {
handleItem(item);
if (performance.now() - lastYield > budgetMs) {
await yieldToMain();
lastYield = performance.now();
}
}
}
Поступайтеся потоком через scheduler.yield там, де він є
scheduler.yield() продовжує ваш код як пріоритетне продовження, раніше за інші завдання зі схожим пріоритетом у черзі. MDN позначає обмежену доступність: Chrome та Edge 129+, Firefox 142+, у Safari підтримки немає. Перевірка наявності API із запасним варіантом на setTimeout() обов’язкова. Запасний варіант теж ділить завдання, але втрачає пріоритет. web.dev більше не радить isInputPending() і рекомендує поступатися потоком у будь-якому разі. Помічник для обробки частинами тримає кожне завдання в межах 40 мілісекунд, тож дотик посеред циклу чекає лише одне коротке завдання.
Гідратація та острови
Гідратація всієї сторінки виконує все дерево компонентів під час завантаження, включно зі статичним вмістом. Взаємодії в цей час отримують великі затримки введення. Рендерте статичний вміст як HTML, гідратуйте лише інтерактивні компоненти й відкладайте їх до простою браузера або появи на екрані. В Astro директиви задають момент гідратації кожного острова:
---
import SearchBox from '../components/SearchBox.jsx';
import Comments from '../components/Comments.jsx';
import PriceChart from '../components/PriceChart.jsx';
---
<SearchBox client:idle={{ timeout: 2000 }} />
<Comments client:visible={{ rootMargin: "200px" }} />
<PriceChart client:media="(min-width: 960px)" />
Компоненти без директиви не надсилають JavaScript. React Server Components і часткова гідратація в інших фреймворках прагнуть того самого результату, але його треба вимірювати, а не припускати. Перевірте й сторонні скрипти: видаліть невикористовувані теги, решту завантажуйте в простої браузера й стежте за їхніми URL у longestScript.
Presentation delay
Вартість відмальовування зростає разом із розміром DOM. Тримайте DOM компактним, використовуйте content-visibility: auto для довгих розділів поза екраном і не читайте offsetHeight одразу після запису стилів, бо це спричиняє синхронну розкладку. Важкий розбір або сортування даних переносьте у Web Worker.
CLS: резервуйте місце та стабілізуйте шрифти
Оцінка зсуву макета дорівнює добутку частки ураженої площі на частку відстані. Зсуви з інтервалом менше 1 секунди об’єднуються в сесійне вікно тривалістю до 5 секунд, і CLS дорівнює найбільшому вікну. Зсуви протягом 500 мілісекунд після дотику або натискання клавіші не враховуються. Посібник з оптимізації CLS перелічує типові причини:
- Медіа без розмірів: задайте
widthіheightабоaspect-ratio, щоб місце було зарезервоване. - Реклама, вбудовані блоки й банери: резервуйте слот через
min-heightі ніколи не вставляйте пізній вміст над тим, що читає користувач. - Вебшрифти: використовуйте
font-display: optionalабоswapіз запасним шрифтом, налаштованим черезsize-adjustі перевизначення ascent, descent та line-gap, і завантажуйте критичні шрифти через preload. - Анімації: анімуйте
transformіopacity, а неtop,left,widthчиheight. Композитні трансформації не враховуються в CLS. - Навігація назад і вперед: відновлення з bfcache показує завантажену сторінку без зсувів, тож зберігайте сторінки придатними для нього.
.ad-slot {
min-height: 250px;
}
@font-face {
font-family: "Brand Sans Fallback";
src: local("Arial");
size-adjust: 104%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}
body {
font-family: "Brand Sans", "Brand Sans Fallback", sans-serif;
}
.toast {
transform: translateY(100%);
transition: transform 200ms ease-out;
}
.toast.is-visible {
transform: translateY(0);
}
Відсотки перевизначень тут умовні. Обчисліть їх за метриками вашого справжнього шрифту й порівняйте обидва шрифти в браузері.
Що виправляти насамперед
Почніть із метрики, яка не проходить у групах URL із найбільшим трафіком, на мобільних пристроях, якщо десктоп не є вашою основною аудиторією. Усередині метрики працюйте зі складовою або фазою, що має найбільшу частку на p75. Спершу викочуйте дешеві виправлення: fetchpriority, відмова від лінивого завантаження hero і розміри зображень займають хвилини, а острови чи серверний рендеринг потребують запланованої міграції.
| Сигнал у полі | Імовірна причина | Перше виправлення | Чим підтвердити |
|---|---|---|---|
| LCP, домінує TTFB | HTML без кешу, редиректи | Кеш на edge, без ланцюжків редиректів | Таймінги curl |
| LCP, домінує затримка завантаження | Пізнє виявлення, лінивий hero, клієнтський рендеринг | <img> у HTML, fetchpriority |
Мережевий waterfall |
| LCP, домінує тривалість завантаження | Завелике зображення, старий формат | AVIF або WebP, srcset, CDN |
lcpResourceEntry |
| LCP, домінує затримка відмальовування | Блокувальні CSS, скрипти чи шрифти | Критичний CSS, відкладені скрипти | Трейс Performance |
| INP, затримка введення під час завантаження | Гідратація, сторонні теги | Острови, відкладена гідратація | loadState |
| INP, довга обробка | Важкий обробник | Спершу відмальовування, yield, worker | longestScript |
| INP, довга презентація | Великий DOM, примусова розкладка | Менший DOM, content-visibility |
Таблиця взаємодій |
| CLS під час завантаження | Медіа без розмірів, зміна шрифту | Розміри, налаштований запасний шрифт | largestShiftTarget |
| CLS після завантаження | Пізній вміст, анімації розкладки | Зарезервовані слоти, transform |
Вкладка зсувів макета |
Перевірка виправлення та захист від регресій
Підтверджуйте кожну зміну двічі. У лабораторії порівняйте трейси Performance до й після з уповільненням за польовими даними та переконайтеся, що потрібна складова зменшилася. У полі стежте за p75 у своєму RUM для зачепленого шаблону, який реагує за кілька днів, і дайте CrUX наздогнати зміни за 28 днів. Потім натисніть «Почати відстеження» в Search Console, щоб запустити 28-денну перевірку.
Тримайте бюджет Lighthouse у CI, щоб ловити нові блокувальні скрипти, зображення без розмірів і стрибки Total Blocking Time. Він не бачить реальних взаємодій, тож налаштуйте й сповіщення за польовим INP, а релізи позначайте на дашборді RUM.
Чек-лист
- Вивчіть польові дані в Search Console і PageSpeed Insights для мобільних і десктопа.
- Запишіть, яка метрика не проходить на p75 у групах URL із найбільшим трафіком.
- Підключіть attribution-збірку
web-vitalsі надсилайте beacon під часvisibilitychange. - Агрегуйте RUM за шаблоном, типом пристрою й
target. - Знайдіть домінантну складову LCP до вибору виправлення.
- Розмістіть hero в HTML із
fetchpriority="high", без лінивого завантаження й з правильнимsrcset. - Кешуйте HTML на edge і тримайте TTFB у межах 0,8 секунди.
- Діліть довгі обробники через
scheduler.yield()із запаснимsetTimeout(). - Гідратуйте лише інтерактивні острови й перевіряйте сторонні скрипти.
- Резервуйте місце для медіа, реклами й банерів і налаштовуйте запасні шрифти.
- Перевіряйте виправлення в уповільненому трейсі, потім у RUM, потім у CrUX.