Пачынайце з палявых даных, а не з ацэнкі 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.