Фронтенд-продуктивність і Core Web Vitals
Зміст
Класична сцена: перф-звіт по бекенду зелений, p95 тримається в межах кількох сотень мілісекунд, а підтримка того ж дня приймає скарги «сайт гальмує». Обидві сторони кажуть правду — просто міряють різні речі. Серверні метрики закінчуються на першому байті відповіді; користувацький досвід із нього тільки починається. Далі браузер має скачати решту ресурсів, розібрати розмітку, виконати JavaScript, порахувати геометрію й намалювати кадр — і саме там живе більшість скарг на «повільно».
Ця глава — єдиний повний виклад клієнтської продуктивності (client-side performance) на сайті: решта глав про метрики поверх браузера посилаються сюди. Механіка, поверх якої стоять ці метрики — життєвий цикл запиту, конвеєр рендерингу, HTTP, Waterfall як інструмент DevTools, — розібрана в розділі «Веб і мережі для AQA»; тут ідеться про метрики над нею.
Швидкий бекенд ще не швидка сторінка
Зона відповідальності бекенда закінчується на (time to first byte) — часі від старту запиту до першого байта відповіді. Це верхня межа впливу сервера й водночас нижня межа будь-якої клієнтської метрики: і містять у собі вивантаження попередньої сторінки, встановлення зʼєднання, редиректи й решту складників TTFB. Нульовий бекенд не дає нульового LCP. Дзеркально: TTFB і FCP — діагностичні метрики поганого LCP, перша вказує на повільну відповідь сервера, друга — на ресурси, що блокують рендеринг.
Клієнтський набір покриває три боки досвіду — завантаження, інтерактивність і візуальну стабільність. Двох останніх бекенд не бачить узагалі, і залежать вони від того, чого немає в жодному серверному звіті: від пристрою, мережі, фонових процесів і поведінки користувача. Заразом варто відмовитися від звички міряти «час завантаження» подіями load і DOMContentLoaded — вони погані рівно тим, що не обовʼязково відповідають тому, що користувач бачить на екрані. Ціна повільності вимірна, але цифри цитують з атрибуцією, як чужі кейс-стаді: BBC втрачали ще 10 % користувачів на кожну додаткову секунду завантаження, покращення LCP на 31 % дало Vodafone +8 % продажів.
Core Web Vitals: набір, пороги і 75-й перцентиль
Web Vitals — ініціатива Google, сенс якої в тому, щоб власнику сайту не треба було бути експертом з продуктивності. — підмножина, що стосується всіх сторінок і виводиться в усіх інструментах Google; кожна метрика набору вимірна в полі й описує реальний результат користувача.
| Метрика | Що міряє | «good» | «poor» |
|---|---|---|---|
| LCP | завантаження основного вмісту | ≤ 2500 мс | понад 4000 мс |
| INP | швидкість відгуку на дії користувача | ≤ 200 мс | понад 500 мс |
| CLS | візуальна стабільність | ≤ 0,1 | понад 0,25 |
Порогів у кожної метрики два, а не один: між ними лежить категорія «needs improvement». Ті самі три категорії експортує бібліотека web-vitals константами — LCPThresholds дорівнює [ 2500, 4000 ], INPThresholds — [ 200, 500 ], CLSThresholds — [ 0.1, 0.25 ].
Окремо про межу включення, бо саме її читач копіює в поріг свого тесту. web.dev формулює пороги із включенням межі («200 мілісекунд або менше»), а Google Search Central у документі про ранжування переказує їх суворіше — «less than 200 milliseconds». Розбіжність не косметична: за методикою порогів рівно 200 мс — це ще «good», і цю ж редакцію підтверджують константи бібліотеки. Тому в поріг тесту беруть «або менше», а формулювання документа про ранжування читають як популярний переказ, а не як означення метрики.
Друга половина правила — зріз розподілу. Ціль виконується не «в середньому», а на 75-му завантажень, окремо для мобільних і десктопних, і сторінка проходить лише коли на ньому виконано ціль по всіх трьох метриках одночасно. Вибір перцентиля — компроміс: на 100 візитах достатньо пʼятьох викидів, щоб зіпсувати 95-й перцентиль, і треба вже 25, щоб зрушити 75-й. Самі пороги виводять із досліджень сприйняття (де це діапазон, а не одне число) і одночасно з досяжності. Склад набору не вічний — 2024 року FID вивели, коли стабільною метрикою став .
LCP: коли зʼявився основний вміст
LCP (Largest Contentful Paint) звітує час рендерингу найбільшого зображення, текстового блоку або відео у видимій області — відносно моменту переходу на сторінку. Гіпотеза, на якій усе стоїть, названа в специфікації прямо: найбільше малювання під час завантаження ймовірно позначає значущу для користувача подію. Кандидати — обмежений перелік із пʼяти класів елементів, і рахується лише видима у viewport частина.
Найважливіше для розуміння виміру — метрика змінюється в часі. Браузер віддає новий запис щоразу, коли найбільший елемент змінюється, тож в аналітику треба слати останній запис, а не перший. Видалення елемента з viewport чи навіть із DOM звання найбільшого з нього не знімає — знімає лише поява більшого. Правило зроблене заради каруселей, але воно ж ламає виміри на сайтах зі сплеш-екранами-плейсхолдерами.
Далі — пастки, які бʼють саме по автотестах:
- Перша ж взаємодія зупиняє вимір. Алгоритм зупиняється на скролі або вводі, бо саме вони найімовірніше додають новий вміст. Тест, який одразу після
gotoскролить сторінку, вимір руйнує; якщо ввід стався до появи основного вмісту, результату може не бути взагалі. - Фонова вкладка мовчить. У фоні записи не видаються до фокуса, тож для таких сторінок інструменти Google LCP не звітують.
- SPA не скидає метрику. LCP привʼязаний до завантаження документа й не скидається ні при відновленні з bfcache, ні при навігації в межах того самого документа.
- зображення без
Timing-Allow-Originне віддавали час рендерингу — звідси на вигляд неможлива картина «LCP раніший за FCP».
Специфікація сама визнає, що API стоїть на евристиках і схильний до похибок. Звідси й розходження з лабораторією: елемент, який лабораторія визнала LCP-елементом, може бути не тим, що бачить користувач — через розмір екрана, персоналізацію, A/B-тест, набір шрифтів у системі. Якщо причина поганого LCP неясна, розкладає його на підчастини — TTFB зображення, до початку завантаження, тривалість завантаження й затримка рендерингу.
INP: чи встигає сторінка відповісти
INP (Interaction to Next Paint) оцінює чутливість сторінки на дії користувача: дивиться на затримку всіх взаємодій за візит і віддає одне число. Мотив арифметичний — за даними Chrome, 90 % часу на сторінці користувач проводить після її завантаження. Відповіддю вважається візуальний відгук у наступному намальованому кадрі, а не завершення всіх наслідків взаємодії: мережеві запити й асинхронні оновлення UI в INP не входять.
Взаємодія — це група обробників одного логічного жесту (pointerdown, pointerup, click), яка збирається за interactionId. Затримка розкладається на три частини.
Підсумкове число — найдовша взаємодія без урахування викидів: на сторінках із багатьма взаємодіями відкидається одна найгірша на кожні 50, а здебільшого взаємодій менше за 50, тож звітується просто найгірша. Рахується INP у момент виходу зі сторінки. Пороги: до 200 мс включно — добре, понад 200 і до 500 мс включно — потребує покращення, понад 500 мс — погано.
Тепер найдорожче для AQA. INP спостерігає лише три типи взаємодій: клік мишею, тап і натискання клавіші; наведення, зум і скрол не враховуються, а неперервні події (mousemove, wheel, drag) виключено, бо API не має способу осмислено їх агрегувати. І головне обмеження ідеї «зміряти INP автотестом»: подія з isTrusted === false у вимір не потрапляє взагалі. Синтетична подія, породжена скриптом через dispatchEvent, метрики не дає — жодної. Тому сторінка може взагалі не мати значення INP, і серед названих причин прямо стоїть «сторінку відкрив бот або headless-браузер, якому не прописали взаємодій».
Лабораторія тут особливо слабка: INP міряє чутливість саме в ті моменти, коли користувач сам вирішив взаємодіяти, і скриптована поведінка моменту не вгадує. Без мобільного viewport браузер додає 300 мс затримки після тапу — вона входить в INP, але не в . Історична довідка, яку досі питають: INP — наступник FID, що міряв затримку введення лише першої взаємодії.
CLS: чому кнопка тікає з-під пальця
(Cumulative Layout Shift) кількісно описує візуальну стабільність: як часто користувач бачить несподівані зсуви розкладки, які збивають читання й змушують натиснути не те посилання. Зсув фіксується, коли видимий елемент змінює початкову позицію між двома кадрами; поява нового елемента чи зміна розміру наявного зсувом не є — доти, доки це не посуває інші видимі елементи.
Оцінка одного зсуву рахується за нормативною формулою специфікації: частка впливу × частка відстані (приклад із джерела: 0,75 × 0,25 = 0,1875). Перший множник — обʼєднання видимих площ нестабільних елементів у поточному й попередньому кадрі, поділене на площу viewport; другий — найбільше зміщення, поділене на більший вимір viewport.
А от зведення окремих зсувів в одне число — місце, де половина статей в інтернеті застаріла. CLS — це найбільша серія оцінок зсуву за життєвий цикл сторінки, а не сума всіх зсувів. Серією (session window) вважають зсуви підряд із проміжком менше 1 секунди між ними й загальною тривалістю вікна не більше 5 секунд. Сумою CLS справді був раніше, і специфікація й досі описує його як суму — але сама позначає ці означення ненормативними, бо API таких величин не віддає: він віддає лише значення окремого зсуву на кадр, а зведення в метрику робить . Нормативний внесок специфікації — формула одного зсуву й вікно виключення за вводом: зсуви протягом 500 мс після вводу отримують hadRecentInput і можуть бути виключені, бо поганий не всякий зсув, а той, якого користувач не очікує. Прапорець ставиться лише для дискретних подій; скрол і зум «недавнім вводом» не вважаються.
Тепер пастка, на якій ламаються баг-репорти. API віддає до пʼяти елементів, що найбільше долучилися до зсуву, — але sources показує зсунуті елементи, а не той, що їх зсунув. Якщо банер, який довантажився зверху, штовхнув увесь вміст під собою, у списку буде тільки штовхнуте: специфікація прямо каже, що визначити справжню першопричину браузер не може. Тож «винний елемент» зі звіту читають як «постраждалий» і шукають причину вище за потоком.
Причини передбачувані — асинхронне довантаження, зображення без заданих розмірів, шрифт за , сторонні банери, — і систематично цього не видно в розробці: тестові зображення вже в кеші, локальні занадто швидкі. Перед автотестом варто памʼятати, що метрика міряється лише в Chromium і що на сторінці з iframe функція onCLS() формально міряє DCLS: API не бачать вмісту фреймів навіть свого .
Допоміжні метрики: FCP, довгі задачі й TBT
FCP (First Contentful Paint) позначає перший момент, коли користувач узагалі щось бачить: текст, зображення, <svg> або небіле <canvas>. Ціль — 1,8 секунди або менше, понад 3,0 секунди — «poor». У Core Web Vitals FCP не входить: це допоміжна метрика для діагностики поганого LCP, і вона навмисно грубіша за людське око — текст кольору фону користувач не бачить, а для FCP він рахується. FCP відповідає на питання «чи взагалі щось відбувається?», LCP — «чи готове те, заради чого я прийшов»: сплеш-екран FCP закриває, а користувачеві не дає нічого.
(long task) — задача циклу подій тривалістю понад 50 мс. Поки вона виконується, головний потік заблокований, і взаємодія посеред неї просто чекає: браузер не може перервати вже розпочату задачу. Поріг виведено арифметично з моделі RAIL, яка вимагає відповіді на ввід менш ніж за 100 мс — 50 мс на добігання поточної задачі плюс 50 мс на саму реакцію. Наслідок, який найчастіше й описують у баг-репорті: сторінка виглядає повністю намальованою й не дає з собою взаємодіяти. У панелі Performance довгу задачу позначає червоний трикутник, і панель веде від події до її ініціатора аж до рядка коду, окремо попереджаючи про примусовий reflow.
TBT (Total Blocking Time) зводить довгі задачі в одне число: сумарний час після FCP, коли головний потік був заблокований настільки, що сторінка не могла реагувати на ввід. Блокувальним вважається час задачі понад 50 мс, і TBT — сума таких надлишків; ціль — менше 200 мс на середньому мобільному залізі. Метрика лабораторна: у полі її міряти не радять через розкид. Вона краща за старий TTI, бо менш чутлива до викидів: три задачі по 51 мс і одна задача на 10 секунд відсувають TTI однаково, хоча TBT дає 3 мс проти 9950 мс.
Головне обмеження: TBT — розумний лабораторний INP, але не його заміна. Він дає хибні тривоги (позначає проблему там, де користувач у цей момент не взаємодіє) і має пропуски (не ловить проблем від самих взаємодій). Саме тому в Lighthouse 10 TBT важить найбільше серед метрик — 30 % підсумкового балу: сам INP інструмент виміряти не може.
Лабораторія і поле: два різні виміри
Лабораторні інструменти збирають дані в керованому середовищі із заздалегідь заданими пристроєм і мережею — таке середовище ще називають синтетичним. Польові збирають дані від реальних відвідувачів; й RUM (Real User Monitoring) — взаємозамінні назви.
Мета лабораторного тесту — прибрати якнайбільше змінних, щоб результат був відтворюваним: це найкращий спосіб перевірити фічу до релізу й зловити заздалегідь. Але кероване середовище навмисно не відтворює реальний розкид: один пристрій, одна мережа, одна географія, майже завжди холодний кеш і жодного bfcache.
Найголовніше про польові дані: це розподіл, а не число. Якщо інструмент показує одне число — це точка розподілу, і для Core Web Vitals це 75-й перцентиль; тому лабораторні 3,0 с проти польових 1,8 с — не помилка вимірювання. Звідси правило пріоритету: коли є обидва набори, пріоритети ставлять за польовими даними, і лабораторний вимір польового не заміняє. Зворотний бік теж корисний: поле бачить лише тих, кому сайт узагалі завантажився, і саме лабораторія допомагає дотягтися до повільних мереж та слабких пристроїв.
Якщо піднімати власний RUM, три правила економлять місяці:
- Не рахуйте середнє — воно не описує жодного реального сеансу, а невелика група на дуже повільних мережах може взагалі його не зрушити. Звіт має показувати кожну метрику на 75-му перцентилі, а поруч — 90-й і 95-й.
- Стежте за моментом відправки: CLS остаточний лише коли сторінка починає вивантажуватися, а
beforeunloadіunloadненадійні й можуть вибити сторінку з bfcache. Правильний момент —visibilitychangeпри переході вhidden. - Не діліть метрики «до і після дати деплою» — кешування на рівні HTTP, service worker чи CDN ламає цю межу; кожному потрібна унікальна версія.
Готове поле «безкоштовно» дає CrUX (Chrome User Experience Report) — датасет про те, як реальні користувачі Chrome переживають популярні сторінки вебу. Він показує розподіл за 28-денне вікно й віддає метрики лише як статистичні агрегації; сирих значень окремих сеансів API не дає взагалі, а p75 читається буквально: щонайменше 75 % завантажень мали значення не більше за наведене. Головне обмеження: у датасеті є не всі сторінки — сторінка має бути публічно знаходною і мати достатньо відвідувачів для статистичної значущості. Для нового або малотрафікового сайту польових даних там просто не буде, і це не діагноз «сайт швидкий».
Бал продуктивності Lighthouse: що це число означає
Підсумковий бал — зважене середнє балів окремих метрик, і самих балів метрик у звіті не показують. У Lighthouse 10 ваги такі: FCP — 10 %, Speed Index — 10 %, LCP — 25 %, TBT — 30 %, CLS — 25 %; у Lighthouse 8 набір був інший. Практичний наслідок жорсткий: бали різних версій інструмента незіставні, і «було 72, стало 68» після оновлення нічого не означає.
Найважливіше про природу числа: сире значення метрики перетворюється на бал 0–100 за положенням у логнормальному розподілі, побудованому на даних HTTP Archive. Тобто бал Lighthouse — це позиція відносно решти вебу, а не абсолютна оцінка: 25-й перцентиль HTTP Archive дає бал 50, 8-й перцентиль — бал 90, а приблизно з 0,96 починається зона спадної віддачі, де підняти бал із 99 до 100 приблизно так само дорого, як із 90 до 94.
І окремо про стрибучість, бо на неї спирається головне процесне правило. Коливання балу здебільшого спричинені не інструментом, а зміною умов вимірювання: A/B-тести й різні рекламні креативи, маршрутизація трафіку, різні пристрої, розширення браузера, антивірус. Сам інструмент радить думати про продуктивність як про розподіл балів, а не про одне число — це і є пряма підстава правила «один прогін Lighthouse не є гейтом». Як зробити з цього робочий гейт у — тема глави «Performance-тести в CI/CD».
Waterfall у машиночитному вигляді: Resource Timing і Navigation Timing
Колонка Waterfall у вкладці Network — інструмент розділу «Веб і мережі для AQA»; тут важливо інше: той самий таймлайн доступний у коді. Специфікація Resource Timing означує PerformanceResourceTiming — повні мітки часу кожного ресурсу, і поля цього інтерфейсу утворюють таймлайн завантаження: редиректи, fetchStart, DNS, зʼєднання, requestStart, responseStart, responseEnd. Саморобний замір через onload так не вміє — він не розкладає час на фази й не бачить ресурсів, оголошених у розмітці.
Запис несе не тільки час: розміри тіла, код відповіді, протокол — і машинну позначку blocking або non-blocking, тобто «що блокує рендеринг» видно прямо з таймлайна. Звідси готові перевірки: стиснення працює, якщо decodedBodySize не дорівнює encodedBodySize, а transferSize рівний нулю означає влучання в кеш.
Три пастки роблять цей таймлайн брехливим:
- Ресурс може не потрапити в таймлайн узагалі. Ресурс, зарубаний отримання (mixed content, CORS, CSP), туди не додається — його там просто немає, а не «є з нульовим часом».
- Крос-оріджин ресурс дає непрозорий запис. Без дозволу таймінгу занулено редиректи, DNS, зʼєднання, TLS, запит і початок відповіді, а для крос-оріджин відповіді з CORS — ще й розміри тіла та тип вмісту; лишається сама тривалість — саме тому сторонні скрипти виглядають «одним суцільним блоком». Маскування знімає заголовок
Timing-Allow-Origin, але навіть із дозволом браузер має право лишити розміри нульовими. - Буфер записів скінченний. Типово буферизується лише 250 записів ресурсів, тож довгий сценарій із сотнями запитів мовчки втрачає хвіст.
Ще дрібниця з великими наслідками: responseStart може бути проміжною відповіддю (103 Early Hints), тож наївне «TTFB — це responseStart - requestStart» на такій сторінці міряє не те.
Життєвий цикл самого документа описує сусідня специфікація — Navigation Timing. Її запис у таймлайні один, дістається як перший елемент performance.getEntriesByType("navigation") і успадковує все від запису ресурсу. Крім звичних міток (domInteractive, domContentLoadedEventStart, domComplete, loadEventEnd) у ньому є три поля, що прямо стосуються достовірності виміру: тип навігації з рівно трьома значеннями — navigate, reload, back_forward, за яким відсіюють прогони «назад/вперед» від холодного завантаження; confidence — обʼєкт, чиє поле value дорівнює high або low, де low означає, що метрики можуть не описувати реальний досвід на цьому пристрої (канонічна підстава тези «одне вимірювання не є оцінкою»); і redirectCount. Окремої мітки «взяли з кешу» тут немає: робота з HTTP-кешем уже врахована в requestStart, responseStart, responseEnd.
Про правила кешування — глава «Кешування», про фази запиту як явище — «Життєвий цикл запиту», про читання Waterfall руками — «DevTools: вкладка Network і дебаг».
Виміри в автотестах: Performance API
— не одна специфікація, а група стандартів. Одиниця виміру в ній одна: PerformanceEntry з полями name, duration, startTime і типом. Родина широка — від LargestContentfulPaint і LayoutShift до PerformanceServerTiming, який витягує серверні метрики із заголовка Server-Timing, тобто робить місток між фронтендною метрикою й серверною стороною.
Перше, що треба зрозуміти, — чому тут не годиться Date.now(). Date рахує час за системним годинником, а той зсувається й підкручується: замір «до і після» може дати додатне число, відʼємне або нуль, і це сказано в специфікації прямо. Звідси правило поділу: монотонний годинник — для вимірів, настінний — для показу користувачеві. performance.now() повертає тривалість від початку відліку, і різниця двох послідовних викликів відʼємною бути не може. Записи читають через PerformanceObserver — він повідомляє про нові в момент появи, а підтримку типу перевіряють через supportedEntryTypes, а не try/catch.
І тепер найдорожче застереження всієї теми: зняти запис PerformanceObserver ще не означає виміряти метрику. Джерело перелічує чотири розбіжності поіменно на прикладі FCP: API віддає запис і для сторінки у фоновій вкладці, хоча метрика такі сторінки має ігнорувати; при відновленні з bfcache API запису не дає, а метрику міряти треба; крос-оріджин iframe можуть не потрапити у виміри; API рахує від початку навігації, а для попередньо відрендерених сторінок відлік має йти від activationStart. Бібліотека web-vitals бере ці розбіжності на себе — крім випадку з iframe. І є випадки, коли зміряти метрику в JavaScript неможливо взагалі.
Наша практика (не канон). Далі — те, як це роблять команди, з якими ми працювали; окремого стандарту чи специфікації під це немає. Спокуса «додати перф-перевірку в e2e» велика, але корисним виявляється вузький зріз: детерміновані факти з таймлайна, а не самі Core Web Vitals. Причини названі вище: INP у сценарії вимірний лише настільки, наскільки вдається зімітувати реалістичні взаємодії, LCP руйнується першим же скролом сценарію, CLS у сценарії неповний. Натомість добре працюють перевірки, що проходять або ні незалежно від заліза : кількість редиректів, вага ресурсів, блокування рендерингу, влучання в кеш.
import { test, expect } from '@playwright/test';
test('головна віддається без редиректів і без зайвої ваги', async ({ page }) => {
await page.goto('/', { waitUntil: 'load' });
const nav = await page.evaluate(() => {
const [entry] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
return { type: entry.type, redirectCount: entry.redirectCount };
});
expect(nav.type).toBe('navigate');
expect(nav.redirectCount).toBe(0);
const heavyScripts = await page.evaluate(() =>
(performance.getEntriesByType('resource') as PerformanceResourceTiming[])
.filter((r) => r.initiatorType === 'script' && r.transferSize > 300_000)
.map((r) => r.name),
);
expect(heavyScripts, `завеликі скрипти: ${heavyScripts.join(', ')}`).toHaveLength(0);
});
Три застереження до цього коду, усі з попередніх підрозділів. transferSize рівний нулю означає влучання в кеш, тож на прогрітому контексті перевірка мовчатиме — контекст має бути свіжим. Нульові розміри віддає й крос-оріджин ресурс без Timing-Allow-Origin, тож саме сторонні скрипти такий фільтр пропускає. І буфер ресурсів типово тримає 250 записів, тож у довгому сценарії читати його треба одразу після завантаження, а не наприкінці тесту.
Вплив на SEO: досвід сторінки в пошуку
Основні системи ранжування Google заохочують вміст, що дає добрий (page experience), і концентруватися на одному-двох аспектах Google прямо не радить. Самоперевірка, яку він пропонує, — шість питань: чи добрі Core Web Vitals; чи віддаються сторінки безпечно; чи коректно вміст показується на мобільних; чи немає надміру реклами; чи немає навʼязливих перекривних вікон; чи легко відрізнити основний вміст від решти. Перелік прямо названо не вичерпним.
Що з цього справді впливає на позиції: Core Web Vitals системи ранжування використовують — це підтверджено прямо, а дані для чинника беруться з CrUX. Формулювання рекомендації варто цитувати акуратно: Google «радить» мати добрі Core Web Vitals і каже, що вони «узгоджені» з тим, що заохочують системи ранжування, — а не «гарантують вищу позицію». Єдиного «сигналу досвіду сторінки» не існує, а оцінка переважно посторінкова.
І межа, яку варто памʼятати дослівно, бо саме на ній будуються найгучніші обіцянки в тендерах: зелений звіт не гарантує високих позицій. Гнатися за ідеальним балом заради SEO Google називає сумнівним витрачанням часу. Релевантність переважає: погана продуктивність не викине релевантну сторінку з видачі.
Типові помилки
- «Бекенд тримає p95 200 мс — зі швидкістю все добре» → виглядає як закритий пункт, а насправді TTFB лише нижня межа клієнтських метрик: інтерактивності й візуальної стабільності бекенд не бачить.
- «CLS — це сума всіх зсувів за сесію» → виглядає як означення зі специфікації, а насправді сумою метрика була раніше, і саме означення сумою специфікація позначає ненормативним.
- «У
sourcesзаписаний елемент — ось хто зсунув розкладку» → виглядає як готовий баг-репорт, а насправді там зсунуті елементи; причину браузер визначити не може й не намагається. - «Ми міряємо INP автотестом» → виглядає як метрики, а насправді в лабораторії INP вимірний лише настільки, наскільки вдається зімітувати реалістичні взаємодії, а подія, породжена скриптом через
dispatchEvent, у вимір не потрапляє взагалі. - «Lighthouse 94 — Core Web Vitals зелені» → виглядає як еквівалент, а насправді бал це позиція відносно розподілу HTTP Archive у лабораторії, а набір польовий і рахується на 75-му перцентилі реальних користувачів.
- «У CrUX по нашому сайту порожньо — значить, усе гаразд» → виглядає як зелений результат, а насправді сторінка просто не набрала трафіку для статистичної значущості.
- «Порахували всі запити через таймлайн ресурсів» → виглядає як повний таймлайн, а насправді буфер типово тримає 250 записів, а крос-оріджин ресурси без
Timing-Allow-Originвіддають лише загальну тривалість.
Підсумок
- починається там, де закінчується серверна. TTFB — верхня межа впливу бекенда й нижня межа будь-якої клієнтської метрики.
- Core Web Vitals — три метрики, дві межі в кожної і один зріз розподілу. Оцінка йде на 75-му перцентилі, окремо для мобільних і десктопних, і сторінка проходить лише коли всі три виконано одночасно.
- Набір польовий за задумом. Лабораторія ловить регресію до релізу, але пріоритети ставлять за польовими даними, а поле — це розподіл, а не число.
- Автотестом міряються не метрики, а факти. INP вимірний лише настільки, наскільки вдається зімітувати реалістичні взаємодії, LCP руйнується взаємодією — зате редиректи, вага ресурсів і влучання в кеш читаються з таймлайна детерміновано.
- Кожен API має названу межу. Запис ще не метрика,
sourcesпоказує постраждалих, а не винних, буфер ресурсів скінченний, а зелений звіт не гарантує ані швидкості для користувача, ані місця у видачі.
Можливі питання
- «Що таке Core Web Vitals і які в них пороги?» — питання на впізнавання. Сильна відповідь називає три метрики з тим, що кожна міряє, дає обидві межі (не лише «good») і одразу додає правило 75-го перцентиля — без нього пороги не мають сенсу.
- «Бекенд швидкий, а користувачі скаржаться на гальма. Де шукати?» — перевіряють, чи розводите ви серверну й клієнтську частини. Очікують згадку, що TTFB лише початок, і перехід до FCP/LCP як діагностики завантаження та довгих задач як причини «намальовано, але не реагує».
- «Як ви поміряєте Core Web Vitals у Playwright?» — пастка на межі інструмента. Слабка відповідь одразу пише код; сильна називає обмеження — INP вимірний лише настільки, наскільки вдається зімітувати реалістичні взаємодії (а подія з
dispatchEventне рахується взагалі), LCP зупиняється на першій взаємодії — і пропонує детерміновані перевірки з таймлайна плюс окремий лабораторний прогін. - «У нас Lighthouse 95, а Search Console показує "needs improvement". Хто бреше?» — дивляться, чи розумієте ви різницю лабораторії й поля. Ніхто не бреше: одне число з керованого середовища, друге — 75-й перцентиль реальних користувачів за 28-денне вікно.
- «CLS у звіті 0,3, у списку елементів — блок із текстом. Це він винен?» — питання рівно про атрибуцію. Правильна відповідь: у списку зсунуті елементи, а не винуватець; шукати треба те, що зʼявилося або змінило розмір вище за потоком.
Джерела
Швидкий бекенд ще не швидка сторінка
- web.dev — Why does speed matter? — утримання й доведення дії до кінця, чужі кейс-стаді BBC/Vodafone/redBus з атрибуцією, мегабайти коду та обмеження мобільних пристроїв.
- web.dev — Time to First Byte (TTFB) — TTFB як час від старту запиту до першого байта відповіді.
- MDN Glossary — Time to first byte (TTFB) — те саме означення в словнику MDN.
- web.dev — Largest Contentful Paint (LCP) — LCP містить у собі TTFB і його складники; чому
loadіDOMContentLoadedне відповідають побаченому. - web.dev — First Contentful Paint (FCP) — FCP так само містить вивантаження попередньої сторінки, зʼєднання, редиректи й TTFB.
- web.dev — Web Vitals — три боки досвіду; TTFB і FCP як діагностичні метрики поганого LCP; залежність від пристрою, мережі й поведінки користувача.
Core Web Vitals: набір, пороги і 75-й перцентиль
- web.dev — Web Vitals — ініціатива та її мета, склад Core Web Vitals, цілі трьох метрик, правило 75-го перцентиля й «усі три одночасно», життєвий цикл метрики і заміна FID на INP.
- web.dev — The research and methodology behind Core Web Vitals thresholds — дві межі кожного порога й категорія «needs improvement», критерії якості й досяжності, частка 10 %, арифметика вибору 75-го перцентиля.
- Google Search Central — Understanding Core Web Vitals and Google search results — редакція порогів через «less than», яку дає документ про ранжування.
- GoogleChrome/web-vitals — README — константи порогів і готова категорія
ratingзамість ручного порівняння.
LCP: коли зʼявився основний вміст
- web.dev — Largest Contentful Paint (LCP) — означення й ціль, класи кандидатів і евристики виключення, правила розміру, поведінка в часі й «слати останній запис», зупинка на взаємодії, фонова вкладка,
Timing-Allow-Origin. - W3C — Largest Contentful Paint — гіпотеза й проєктні цілі метрики, правило «видалення не знімає звання» та його наслідок для сплеш-екранів, відсутність скидання в SPA і при bfcache, визнання евристичності API.
- web.dev — The research and methodology behind Core Web Vitals thresholds — друга межа LCP: понад 4000 мс — «poor».
- web.dev — Why lab and field data can be different (and what to do about it) — пʼять причин, чому лабораторний LCP-елемент не той, і різниця моменту визначення в лабораторії та в полі.
- Chrome for Developers — CrUX API — розкладка LCP на підчастини окремими метриками.
INP: чи встигає сторінка відповісти
- web.dev — Interaction to Next Paint (INP) — означення й мотив «90 % часу після завантаження», три частини затримки, правило відкидання одна на 50 і момент підрахунку, пороги, три типи врахованих взаємодій, причини відсутності значення,
iframeі розходження з CrUX, спадкоємність FID. - W3C — Event Timing API — групування подій через
interactionId, закритий перелік типів і виключення неперервних подій, відкидання подій зisTrusted === false, округленняduration, записи без слухачів, означення першого вводу. - web.dev — The research and methodology behind Core Web Vitals thresholds — обидві межі INP на 75-му перцентилі.
- web.dev — Why lab and field data can be different (and what to do about it) — чому лабораторія не вгадує момент взаємодії й чому 300 мс після тапу входять в INP, але не в TBT.
- GoogleChrome/web-vitals — README — INP не звітується, якщо взаємодії не було.
CLS: чому кнопка тікає з-під пальця
- web.dev — Cumulative Layout Shift (CLS) — означення й ціль, що вважається зсувом, формула та приклад, «найбільша серія» і параметри вікна, попереднє означення сумою, вікно
hadRecentInputі перелік дискретних подій, типові причини й чому дефекту не видно в розробці. - WICG — Layout Instability API — нормативна формула одного зсуву й нормативне вікно 500 мс, ненормативність означення сумою, до пʼяти елементів у
sourcesі пряме застереження, що першопричини браузер визначити не може, ліниві йtakeRecords(). - web.dev — The research and methodology behind Core Web Vitals thresholds — друга межа CLS: понад 0,25 — «poor».
- web.dev — Why lab and field data can be different (and what to do about it) — лабораторний CLS як підмножина польового і вплив персоналізації.
- GoogleChrome/web-vitals — README — CLS лише в Chromium, DCLS замість CLS на сторінці з
iframe, відсутність звіту для сторінки у фоні.
Допоміжні метрики: FCP, довгі задачі й TBT
- web.dev — First Contentful Paint (FCP) — означення, ціль і межа «poor», що вважається вмістом, різниця з LCP і випадок сплеш-екрана.
- W3C — Paint Timing — FP і FCP як метрики поверх малювання, питання «чи взагалі щось відбувається?», грубість метрики щодо тексту кольору фону.
- web.dev — Web Vitals — FCP і TBT як допоміжні метрики поза набором і роль FCP у діагностиці поганого LCP.
- W3C — Long Tasks API — поріг 50 мс і його виведення з моделі RAIL, три випадки, які рахуються довгою задачею, наслідки для вводу, анімацій і скролу.
- web.dev — Total Blocking Time (TBT) — означення й ціль, блокувальний час понад 50 мс, порівняння з TTI на числах, хибні тривоги й пропуски, «проксі, але не заміна», неперервність задачі для браузера.
- Chrome DevTools — Analyze runtime performance — червоний трикутник довгої задачі, шлях від події до рядка коду, попередження про примусовий reflow.
- MDN — Performance API — місце довгих задач і довгих кадрів анімації в родині інтерфейсів.
- web.dev — Why lab and field data can be different (and what to do about it) — чому TBT не міряє «сторінка виглядає готовою».
- Chrome for Developers — Lighthouse Performance scoring — вага TBT 30 % у Lighthouse 10.
- GoogleChrome/web-vitals — README — FCP не звітується для сторінки, завантаженої у фоні.
Лабораторія і поле: два різні виміри
- web.dev — Why lab and field data can be different (and what to do about it) — дві категорії інструментів і RUM як синонім поля, «розподіл, а не число», холодний кеш і bfcache, правило пріоритету польових даних, 28-денне вікно CrUX.
- web.dev — Web Vitals — лабораторія як спосіб зловити регресію до релізу і теза «лабораторний вимір не заміняє польового».
- web.dev — Best practices for measuring Web Vitals in the field — заборона середнього й вибір перцентилів, момент відправки на
visibilitychange, версія деплою замість дати, серверне ділення A/B-груп. - Chrome for Developers — Overview of CrUX — що таке датасет і критерії потрапляння сторінки в нього.
- Chrome for Developers — CrUX API — лише агрегації без сирих значень, режими origin і URL, читання
p75і синтетичність перцентиля. - Chrome for Developers — Lighthouse Performance scoring — порада думати про продуктивність як про розподіл балів.
- GoogleChrome/web-vitals — README — чому власний RUM розходиться з CrUX через вміст фреймів.
Бал продуктивності Lighthouse: що це число означає
- Chrome for Developers — Lighthouse Performance scoring — бал як зважене середнє метрик, ваги версій 10 і 8, логнормальна крива на даних HTTP Archive і дві контрольні точки, зона спадної віддачі, кольори й ціль, пʼять причин коливань і порада про розподіл балів.
- web.dev — Web Vitals — чому найбільшу вагу має лабораторний проксі: INP у симульованому середовищі не вимірний.
- web.dev — Why lab and field data can be different (and what to do about it) — лабораторний тест як один пристрій, одна мережа, одна географія і пріоритет польових даних.
- Google Search Central — Understanding page experience in Google Search results — Lighthouse як інструмент діагностики й застереження проти гонитви за ідеальним балом заради SEO.
Waterfall у машиночитному вигляді: Resource Timing і Navigation Timing
- W3C — Resource Timing — повні мітки часу ресурсу й порядок фаз, обмеженість саморобного заміру через
onload, розміри тіла й позначка блокування рендерингу, три пастки: відсутність зарубаного ресурсу, маскування крос-оріджин запису іTiming-Allow-Origin, скінченний буфер. - MDN — PerformanceResourceTiming — готові формули фаз, машинні перевірки стиснення, кешу й протоколу, типовий розмір буфера 250 записів,
responseStartяк можлива проміжна відповідь. - Chrome DevTools — Network features reference — колонка
Waterfallу вкладці Network як інструментальний бік того самого таймлайна. - W3C — Navigation Timing Level 2 — один запис навігації і спосіб його дістати, мітки життєвого циклу документа, три типи навігації, поле
confidence, відсутність окремої мітки кешу. - MDN — PerformanceNavigationTiming — успадкування від запису ресурсу,
redirectCount,activationStartіnotRestoredReasons. - MDN — Window: load event — подія, якій Navigation Timing проставляє мітки
loadEventStartіloadEventEnd. - MDN — Document: DOMContentLoaded event — подія, вартість обробників якої видно як різниця двох міток.
Виміри в автотестах: Performance API
- MDN — Performance API — група стандартів і таймлайн,
PerformanceEntryяк одиниця виміру, склад родини інтерфейсів іPerformanceServerTimingяк місток до серверних метрик. - MDN — PerformanceObserver — підписка на нові записи,
observe(), список у колбеку,takeRecords()іsupportedEntryTypes. - W3C — High Resolution Time (Level 3) — чому
Date.now()ненадійний, монотонний і настінний годинники,performance.now()як тривалість, необовʼязковість субмілісекундної роздільності. - web.dev — First Contentful Paint (FCP) — чотири розбіжності «запис API ≠ метрика» і випадки, коли зміряти метрику в JavaScript неможливо.
- GoogleChrome/web-vitals — README — бібліотека бере розбіжності на себе, крім
iframe; ціна зайвих викликів і роль прапорцяbuffered. - WICG — Layout Instability API — примусове забирання відкладених записів перед відправкою підсумку.
Вплив на SEO: досвід сторінки в пошуку
- Google Search Central — Understanding page experience in Google Search results — шість питань самоперевірки й невичерпність переліку, підтвердження використання Core Web Vitals у ранжуванні, відсутність єдиного сигналу, посторінкова оцінка, «зелений звіт не гарантує топу» і застереження проти гонитви за балом.
- Google Search Central — Understanding Core Web Vitals and Google search results — Core Web Vitals як набір метрик реального досвіду, обережні формулювання «радимо» і «узгоджено», звіт Core Web Vitals у Search Console.
- Chrome for Developers — Overview of CrUX — дані CrUX як джерело чинника ранжування page experience.
Бекенд тримає p95 у 200 мс, а користувачі кажуть, що сайт гальмує. Хто помиляється?
Ніхто — просто дві сторони міряють різні відрізки шляху. Серверна відповідальність упирається в (time to first byte): момент, коли з мережі прийшов перший байт відповіді. Усе, що людина сприймає словом «завантажилось», відбувається після цього: браузер тягне решту ресурсів, розбирає розмітку, виконує JavaScript, рахує геометрію й малює кадр. TTFB для клієнтських метрик — це підлога, а не результат: і містять його в собі разом із редиректами, встановленням зʼєднання й вивантаженням попередньої сторінки, тож ідеальний сервер сам собою швидкої сторінки не робить. Додатково серверний звіт узагалі сліпий до двох із трьох боків досвіду — чи відповідає сторінка на кліки й чи не стрибає розкладка під пальцем. Правильний хід — не сперечатися з графіком бекенда, а взяти клієнтські метрики й побачити, який саме бік просів.
Які метрики входять у Core Web Vitals і з якими порогами?
Це три метрики реального досвіду: LCP — коли зʼявився основний вміст, — наскільки швидко сторінка відгукується на дії, — наскільки стабільна розкладка. Сильна відповідь називає дві межі в кожної, бо їх саме дві, а між ними лежить категорія «needs improvement»: LCP — 2500 мс і 4000 мс, INP — 200 мс і 500 мс, CLS — 0,1 і 0,25. Нижня межа включна: рівно 200 мс — це ще «good», і ту саму редакцію дають константи бібліотеки web-vitals, хоча документ Google про пошук переказує пороги суворішим формулюванням «менше ніж». Набір не вічний: 2024 року з нього прибрали FID, коли готовою заміною став INP. Одразу після переліку варто додати правило зрізу — без 75-го самі числа порогів нічого не означають.
Чому Core Web Vitals оцінюють на 75-му перцентилі, а не за середнім?
Бо ціль набору — описати досвід більшості реальних відвідувачів, а не одне зручне число. Оцінка береться з 75-го перцентиля завантажень, окремо для мобільних і для десктопних, і сторінка вважається такою, що пройшла, лише коли на цьому зрізі виконано ціль по всіх трьох метриках разом. Вибір саме 75-го — свідомий компроміс між чутливістю й стійкістю: на сотні візитів пʼятьох викидів вистачить, щоб зіпсувати 95-й перцентиль, а щоб зрушити 75-й, таких візитів треба вже близько двадцяти пʼяти. Середнє тут не годиться взагалі: воно не описує жодного конкретного сеансу й спокійно ховає групу користувачів на дуже поганому звʼязку. Практичний наслідок для команди: «у нас середній LCP 1,9 с» — це не відповідь на питання про .
Що таке LCP і який елемент стає LCP-елементом?
LCP (Largest Contentful Paint) звітує момент, коли відрендерився найбільший елемент вмісту у видимій області, відлічуючи від переходу на сторінку. Гіпотеза проста: найбільше малювання під час завантаження найімовірніше і є тим, заради чого користувач прийшов. Кандидатами може бути не будь-що — це закритий перелік класів елементів (зображення, відео, блоки з текстом і подібні), і в розрахунок іде лише та частина елемента, що реально видно у viewport, а не його повний розмір. Специфікація не приховує, що метрика стоїть на евристиках і має похибку. Звідси типове розходження, яке варто назвати на співбесіді: елемент, який LCP-елементом вважає лабораторний прогін, може відрізнятися від того, що бачить жива людина, — через розмір екрана, персоналізацію вмісту, участь в A/B-тесті чи інший набір шрифтів у системі.
Чому в аналітику треба слати останній запис LCP, а не перший?
Тому що це метрика, яка змінюється по ходу завантаження. Щоразу, коли на роль найбільшого елемента претендує новий кандидат, браузер видає ще один запис — і правильним значенням є останній із них, а не той, що прийшов першим. Ключова деталь: звання найбільшого не знімається, якщо елемент прибрали з видимої області чи навіть із DOM; його забирає тільки поява елемента більшого. Зроблено це заради каруселей, де слайди змінюються, але сторінка вже показала користувачеві головне. Побічний ефект теж треба знати: на сайтах, де спершу малюється великий плейсхолдер або сплеш-екран, ця сама логіка дає завищено оптимістичну картину, бо «найбільшим» лишається те, чого користувач уже не бачить.
Автотест одразу після переходу скролить сторінку. Що станеться з виміром LCP?
Вимір буде зіпсований: алгоритм LCP припиняє шукати кандидатів на першій же взаємодії — скролі або натисканні клавіші. Логіка тут своя і розумна: саме такі дії найчастіше додають на сторінку новий вміст, і зараховувати його вже нечесно. Для автоматизації наслідок жорсткий: сценарій, який після переходу одразу гортає сторінку чи щось натискає, робить будь-яке значення LCP непорівнюваним, а якщо ввід трапився раніше за появу основного вмісту, значення може не зʼявитися взагалі. До цього додаються ще три обмеження: у фоновій вкладці записи не видаються до отримання фокуса; усередині SPA метрика не скидається — ані коли перехід відбувається без перезавантаження документа, ані коли сторінку повертають із bfcache; а зображення без заголовка Timing-Allow-Origin не віддавало часу рендерингу — звідси беруться звіти з фізично неможливим «LCP раніший за FCP». Тому для LCP потрібен окремий чистий прогін без дій, а не рядок усередині функціонального сценарію.
Що таке INP і з чого складається затримка однієї взаємодії?
INP (Interaction to Next Paint) описує сторінки: він дивиться на всіх взаємодій за візит і зводить їх в одне число. Мотив суто арифметичний — приблизно девʼять десятих часу на сторінці людина проводить уже після завантаження, і метрики завантаження про цей період не кажуть нічого. Взаємодією вважають групу обробників одного логічного жесту (наприклад, pointerdown, pointerup і click), яку браузер збирає докупи за interactionId. Затримка ділиться на три частини: очікування, поки головний потік звільниться й запуститься перший обробник; час роботи самих обробників; і час до того, як браузер намалює наступний кадр. Відповіддю рахується саме цей кадр — мережеві запити й асинхронні дозавантаження UI в INP не входять. Підсумкове число — найдовша взаємодія з поправкою на викиди: на кожні пʼятдесят взаємодій відкидається одна найгірша, а оскільки їх зазвичай менше, звітується просто найгірша; фіксується значення в момент виходу зі сторінки.
На співбесіді кажуть: «ми міряємо INP автотестом». Що з цим не так?
Найголовніше: породжена скриптом подія до виміру не допускається взагалі — усе, що має isTrusted === false, для INP просто не існує. Тобто «клік» через dispatchEvent не дає ані поганого, ані доброго значення, а рівно нічого; серед офіційно названих причин, чому в сторінки взагалі немає INP, прямо згадують відкриття ботом або headless-браузером без реальних взаємодій. Друге обмеження — перелік типів: враховуються тільки клік мишею, тап і натискання клавіші, а наведення, зум, скрол і неперервні події на кшталт mousemove чи wheel виключені, бо осмислено агрегувати їх нічим. Третє — момент: INP міряє чутливість саме тоді, коли користувач сам вирішив натиснути, а скрипт цього моменту не вгадує. І дрібниця, що псує числа мовчки: без мобільного viewport браузер додає після тапу затримку близько 300 мс, яка в INP входить, а в — ні. Сильна відповідь тут не пише код, а називає межу інструмента й пропонує заміну: лабораторний плюс .
Чим CLS відрізняється від суми всіх зсувів на сторінці?
Тим, що це не сума. CLS — найважча серія зсувів, накопичена за час життя сторінки: зсуви групуються у вікно, доки проміжок між ними менший за секунду, а саме вікно триває не довше пʼяти секунд, і в метрику йде найгірше з таких вікон. Сумою метрика справді була в ранній редакції, і саме тому половина статей в інтернеті досі описує її неправильно; специфікація теж згадує суму, але сама позначає ці означення ненормативними — API віддає лише оцінку окремого зсуву на кадр, а зводить їх у метрику вже . Нормативного в специфікації два пункти: формула одного зсуву (частка впливу помножена на частку відстані) і вікно виключення за вводом. Останнє означає, що зсуви протягом 500 мс після дії користувача позначаються прапорцем hadRecentInput і можуть не рахуватися — поганим є не всякий зсув, а неочікуваний. дістають тільки дискретні події, тож гортання чи масштабування за «свіжий ввід» не проходять.
У звіті CLS перелічені елементи. Це і є винуватці зсуву?
Ні, і це найчастіша помилка в баг-репортах на верстку. API повертає щонайбільше пʼять елементів із найбільшим внеском у зсув, але туди потрапляють ті, кого посунули, а не той, хто посунув. Класика: угорі сторінки довантажився банер і штовхнув униз усе, що під ним; у списку буде текст, картки, кнопки — все, крім банера. Специфікація формулює це прямо: визначити справжню першопричину браузер не може й не намагається. Тому список читають як перелік постраждалих і піднімаються вище за потоком — до елемента, що виник або роздався в розмірі просто перед стрибком: зображення без заданих розмірів, шрифт, який замінився з , сторонній віджет, асинхронно вставлений блок. Окремо варто памʼятати, чому дефекту не видно в розробці: локально картинки вже в кеші, а повертаються надто швидко, щоб щось встигло стрибнути.
Навіщо потрібен FCP, якщо він не входить у Core Web Vitals?
FCP (First Contentful Paint) відзначає кадр, у якому на екрані вперше проступив бодай якийсь вміст: картинка, текст, векторна графіка чи елемент <canvas>, зафарбований не в білий; ціль — уміститися в 1,8 с, а все, що довше за 3,0 с, вважається поганим. У набір він не входить навмисно: його роль — діагностична, він показує, на якому боці шукати причину поганого LCP. Разом із TTFB вони ділять відповідальність: повільний TTFB вказує на сервер, повільний FCP при нормальному TTFB — на ресурси, що блокують рендеринг. Важливо розуміти й межу самої метрики: вона грубіша за людське око, бо текст, намальований кольором фону, для неї вміст, а для користувача — порожній екран. Звідси проста пара питань: FCP каже «чи бодай щось почалося», а LCP — «чи вже на екрані те, по що людина прийшла». Сплеш-екран закриває перше питання й нічогісінько не додає до другого.
Що таке довга задача і чому поріг саме 50 мс?
(long task) — це одиниця роботи в циклі подій, що триває довше за 50 мс. Поки вона триває, головний потік зайнятий, а натискання користувача просто стоїть у черзі: перервати вже розпочату задачу браузер не вміє. Число 50 не взяте зі стелі — воно виведене з моделі RAIL, яка вимагає реакції на ввід швидше ніж за 100 мс: половина цього бюджету відводиться на добігання поточної задачі, половина — на саму обробку. У баг-репортах це виглядає впізнавано: сторінка намальована повністю й здається готовою, а на кліки не реагує. Шукають довгі задачі в панелі Performance — вони позначені червоним трикутником, а сама панель проводить від події до того, хто її ініціював, і далі до конкретного рядка коду, окремо сигналячи про примусовий reflow.
Що таке TBT і чому саме він має найбільшу вагу в Lighthouse?
TBT (Total Blocking Time) — одне число, у яке згортаються всі довгі задачі: підсумовується час після FCP, протягом якого потік був зайнятий настільки, що ввід чекав. Рахують не всю тривалість задачі, а лише надлишок понад 50 мс, і ціль — вкластися в 200 мс на типовому середньому смартфоні. Метрика лабораторна: у полі її не збирають через завеликий розкид. Вона витіснила TTI саме стійкістю до викидів — три дрібні затримки по 51 мс і одна десятисекундна зупинка посувають TTI на однакову величину, тоді як TBT розводить їх як 3 мс проти майже десяти тисяч. У Lighthouse 10 її вага 30 % — найбільша серед метрик, і причина цинічно проста: сам INP інструмент виміряти не може, тож бере найкращий доступний проксі. Але проксі не дорівнює метриці: TBT дає хибні тривоги там, де користувач у цей момент нічого не натискав, і пропускає проблеми, породжені самими взаємодіями.
Lighthouse показує 95, а Search Console — «needs improvement». Хто бреше?
Ніхто: це два різні виміри, і плутати їх — типова помилка. Lighthouse — лабораторний прогін у керованому середовищі: фіксований пристрій, фіксована мережа, одна географія, кеш майже завжди холодний і bfcache не бере участі; його сила в відтворюваності й у тому, що видно ще до релізу. Search Console показує поле — реальних користувачів Chrome, зведених у 75-й перцентиль за вікном у 28 днів. Поле — це розподіл, а не число, і будь-яка окрема цифра в ньому лише позначає точку цього розподілу, тож 3,0 с у лабораторії поруч із 1,8 с у полі не є ані суперечністю, ані збоєм вимірювання. Маючи обидва набори, чергу робіт вибудовують за полем, а лабораторію лишають для діагностики й контролю змін. Зворотний бік теж корисно назвати: поле бачить лише тих, кому сторінка взагалі завантажилася, тож дотягтися до дуже слабких пристроїв і поганих мереж допомагає саме лабораторія.
Що насправді означає число балу продуктивності Lighthouse?
Це зважене середнє балів окремих метрик, причому самі бали метрик у звіті не показуються. Ваги у версії 10 такі: TBT — 30 %, LCP — 25 %, CLS — 25 %, FCP — 10 %, Speed Index — 10 %; у восьмій версії набір був іншим, а отже бали різних версій між собою непорівнянні, і «було 72, стало 68» після оновлення інструмента не означає нічого. Найважливіше про природу числа: сире значення метрики стає балом за тим, куди воно потрапляє в логнормальній кривій, побудованій на даних HTTP Archive. Тобто бал показує, де сторінка стоїть серед решти вебу, а не скільки саме мілісекунд вона витрачає: 50 балів відповідають 25-му перцентилю архіву, 90 балів — 8-му, а приблизно з позначки 0,96 починається зона спадної віддачі, де останній крок до сотні коштує стільки ж роботи, скільки весь шлях від 90 до 94. Звідси й правило про гейти: сама документація пропонує сприймати результат як розкид значень, бо коливання дає не інструмент, а умови — реклама, A/B-тести, маршрутизація трафіку, розширення браузера, антивірус.
«У CrUX по нашому домену порожньо — отже, все гаразд». Що відповісти?
Що це не результат, а відсутність результату. (Chrome User Experience Report) — публічний датасет про досвід, з яким реальні відвідувачі Chrome відкривають сторінки вебу, і в нього потрапляє не все підряд: сторінка має бути публічно знаходною й зібрати стільки відвідувачів, щоб цифри мали статистичну вагу. Для щойно запущеного чи малотрафікового сайту порожнеча означає рівно одне — вибірки не набралося; діагнозу «швидко» вона не ставить. Варто памʼятати й формат: назовні йдуть тільки агрегації за 28-денним вікном, сирих значень окремих сеансів немає взагалі, а позначку p75 читають дослівно — три чверті завантажень уклалися в наведене значення або краще. Практичний вихід у такій ситуації — власний RUM плюс лабораторні прогони, бо чекати, поки зʼявиться чужий датасет, можна довго.
Що з продуктивності має сенс перевіряти в e2e-автотесті, а що ні?
Розділяти варто за одним критерієм: чи буде результат однаковим незалежно від заліза . Самі Core Web Vitals цей критерій не проходять — INP тримається рівно на тому, наскільки правдоподібно сценарій повторює живі дії, LCP гине від першого ж гортання, а CLS за короткий сценарій просто не встигає накопичитися. Натомість із таймлайна ресурсів і навігації читаються цілком детерміновані факти: кількість редиректів, тип навігації, вага окремих ресурсів, позначка блокування рендерингу, факт стиснення (закодований і розкодований розміри тіла різняться) і влучання в кеш (нульовий transferSize). Такі або проходять, або ні — і вони ловлять реальні регресії на кшталт «у бандл заїхала бібліотека на 800 КБ». Три застереження до них теж треба назвати: на прогрітому контексті нульовий розмір означатиме кеш, а не легкий файл; крос-оріджин ресурс без Timing-Allow-Origin теж віддає нулі, тож повз такий фільтр спокійно проходять сторонні скрипти; і буфер записів ресурсів типово вміщає 250 елементів, тож у розлогому сценарії таймлайн знімають відразу по завантаженню, поки хвіст не витиснуло.
Чому знятий через PerformanceObserver запис — це ще не виміряна метрика?
Бо API і метрика мають різні правила, і розходяться вони поіменно. На прикладі FCP їх чотири: API видає запис навіть для сторінки, завантаженої у фоновій вкладці, хоча метрика такі завантаження має ігнорувати; при поверненні з bfcache запису немає, хоча метрику зафіксувати треба; вміст крос-оріджин iframe у виміри може не ввійти; і відлік API веде від початку навігації, тоді як для попередньо відрендерених сторінок його треба вести від activationStart. Готова бібліотека web-vitals усі чотири розбіжності закриває сама, і винятком лишаються тільки фрейми, — саме тому писати власний збирач з нуля зазвичай невигідно. Тут же доречно згадати, чому для вимірів не годиться Date.now(): він читає системний годинник, який підкручується, тож різниця «до і після» може вийти навіть відʼємною; для вимірів потрібен монотонний performance.now(), а підтримку типу записів перевіряють через supportedEntryTypes, а не через try/catch.
Ми піднімаємо власний RUM. Які помилки коштують найдорожче?
Три, і всі системні. Перша — рахувати середнє: воно не описує жодного реального сеансу й може взагалі не зрушити від невеликої групи користувачів на дуже поганому звʼязку; у звіті кожна цифра має бути перцентилем — основний 75-й, поряд 90-й і 95-й, щоб хвіст лишався видимим. Друга — неправильний момент відправки: остаточне значення CLS відоме аж на початку вивантаження сторінки, а покладатися на beforeunload чи unload не можна — вони спрацьовують не завжди й здатні вибити сторінку з bfcache; надійний момент — перехід видимості в hidden. Третя — ділити метрики «до і після дати деплою»: HTTP-кеш, service worker і CDN розмивають цю межу, бо частина користувачів ще довго сидить на старій збірці, тож кожному потрібна власна версія, за якою й ріжуться зрізи. Заразом варто заздалегідь прийняти, що власні числа не зійдуться з CrUX: вибірка інша, вміст фреймів рахується інакше, а вікно агрегації своє.
Чи гарантують зелені Core Web Vitals високі позиції в пошуку?
Ні, і формулювання тут варто відтворювати обережно, бо на цьому будуються найгучніші обіцянки в тендерах. Google підтверджує, що системи ранжування використовують Core Web Vitals і що дані для цього чинника беруться з CrUX, але каже, що радить мати добрі метрики й що вони узгоджені з тим, які сторінки заохочують системи ранжування, — а не що вони забезпечують краще місце у видачі. Окремого зведеного «сигналу досвіду сторінки» не існує, оцінка переважно посторінкова, а сам досвід ширший за три метрики: у самоперевірці Google перелічує ще безпечну віддачу сторінок, коректний показ на мобільних, відсутність надміру реклами й навʼязливих перекривних вікон, легку відмінність основного вмісту від решти — і прямо зазначає, що перелік не вичерпний. Практичний висновок: гонитву за ідеальним балом заради SEO Google сам називає сумнівним витрачанням часу, бо релевантність важить більше: слабка швидкість не здатна прибрати доречну сторінку з результатів.
Три кейси з робочого життя QA: розмитої скарги «сайт гальмує» до конкретної метрики, детерміновані перф-перевірки в Playwright, які не залежать від заліза , і баг-репорт по , що не звинувачує невинного. Скрізь — що дивитися і чому саме це.
Кейс 1. «Сайт гальмує» — переклад скарги на мову метрик
У підтримку приходить тікет без деталей, а в дашборді бекенда все зелене. Перше, що робить сильний QA, — не сперечається з дашбордом, а зʼясовує, який саме бік досвіду просів: поява вмісту, на дії чи стабільність картинки. Формулювання скарги майже завжди містить підказку.
| Що описує користувач | Куди це вказує | Де підтверджувати |
|---|---|---|
| «Довго білий екран, взагалі нічого» | TTFB або FCP | таймлайн ресурсів: чи повільна відповідь сервера, чи ресурси, що блокують рендеринг |
| «Шапка є, а головний блок доїжджає пізніше» | LCP | розкладка LCP на підчастини в CrUX: TTFB зображення, затримка до старту завантаження, саме завантаження, затримка рендерингу |
| «Все намальовано, а кнопка не натискається» | довгі задачі, лабораторно — TBT | панель Performance: червоні трикутники довгих задач і шлях від події до рядка коду |
| «Гальмує вже потім, під час роботи» | INP | поле: лабораторія моменту взаємодії не вгадує, а синтетичні події у вимір не входять |
| «Натиснув не туди, бо все стрибнуло» | CLS | перелік зсунутих елементів плюс пошук причини вище за потоком |
Що дивитися і чому:
- Спершу відсічіть серверну частину. Якщо у нормі, а скарга про «білий екран», відповідальність уже клієнтська: далі шукають ресурси, що затримують перший кадр. Якщо TTFB поганий, то й не полагодиться жодною оптимізацією картинок — він містить TTFB у собі.
- Скарга «не натискається» майже ніколи не про завантаження. Сторінка вже намальована, тобто LCP міг бути чудовим; винен зайнятий головний потік. Це різні власники проблеми й різні виправлення, тож змішувати їх в одному тікеті шкідливо.
- Не давайте перекваліфікувати у «полагодимо TBT». — розумний лабораторний замінник, але він і бʼє на сполох там, де ніхто не взаємодіє, і мовчить про проблеми, породжені самими взаємодіями. Закривати тікет за зеленим TBT, коли скарга саме на відгук, — самообман.
- Одного прогону мало для будь-якого рядка цієї таблиці. Клієнтські метрики залежать від пристрою, мережі й фонових процесів, тож висновок роблять із розподілу, а не з одного заміру на своєму ноутбуці.
Кейс 2. Playwright: перевіряти факти з таймлайна, а не Core Web Vitals
Замовлення від команди звучить як «додай перф-перевірки в e2e». Спокуса зміряти LCP і INP просто в сценарії велика, але обидві метрики в такому вигляді нічого не варті: LCP зупиниться на першому ж скролі сценарію, а INP не отримає жодного значення, бо синтетичні події у вимір не входять. Робочий зріз — детерміновані факти з таймлайна, які або є, або ні, незалежно від того, наскільки повільна машина в CI.
import { test, expect } from '@playwright/test';
// прогін має йти на свіжому контексті: на прогрітому кеші transferSize читається нулем
test('каталог: без редиректів, зі стисненням і без роздутих скриптів', async ({ page }) => {
await page.goto('/catalog', { waitUntil: 'load' });
const nav = await page.evaluate(() => {
const entry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
return { navType: entry.type, redirects: entry.redirectCount };
});
expect(nav.navType).toBe('navigate');
expect(nav.redirects, 'зайвий редирект дорожчає весь LCP').toBe(0);
const resources = await page.evaluate(() =>
(performance.getEntriesByType('resource') as PerformanceResourceTiming[]).map((r) => ({
url: r.name,
kind: r.initiatorType,
transfer: r.transferSize,
encoded: r.encodedBodySize,
decoded: r.decodedBodySize,
})),
);
// буфер типово вміщає 250 записів: якщо впритул — читаємо раніше або чистимо
expect(resources.length, 'таймлайн переповнений, хвіст запитів утрачено').toBeLessThan(250);
const uncompressed = resources.filter(
(r) => r.kind === 'script' && r.decoded > 0 && r.decoded === r.encoded,
);
expect(uncompressed.map((r) => r.url), 'скрипт віддано без стиснення').toEqual([]);
const oversized = resources.filter((r) => r.kind === 'script' && r.transfer > 300_000);
expect(oversized.map((r) => r.url), 'бандл переріс бюджет').toEqual([]);
});
Що дивитися і чому:
transferSizeнуль означає влучання в кеш, а не легкий файл. Тому перевірка ваги має сенс лише на свіжому контексті; на прогрітому вона мовчатиме й пропустить .- Той самий нуль віддає ресурс без
Timing-Allow-Origin. Тобто саме сторонні скрипти крізь такий фільтр проходять безкарно. Це не привід відмовлятися від перевірки, це привід перелічити виняткові домени явно й знати, що вони поза . - Ресурс, зарубаний до завантаження, у таймлайні відсутній зовсім. Заблокований через CSP, mixed content чи CORS файл ви не побачите з нульовим часом — ви не побачите його взагалі, тож «у списку його немає» ще не означає «його не запитували».
- Не рахуйте TTFB як різницю
responseStartіrequestStartнаосліп. Якщо сервер шле проміжну відповідь103 Early Hints, перша мітка припадає на неї, і формула міряє не те, що ви думаєте. - Позначку блокування рендерингу беруть із того самого запису. Це дає перевірку «скільки в нас блокувальних ресурсів на критичному шляху» без жодного й без Lighthouse — а отже, придатну для гейта.
Кейс 3. CLS 0,28: як не звинуватити постраждалого
У полі метрика стабільності просіла, у звіті — 0,28 і перелік елементів, серед яких блок із описом товару. Дефект «блок опису спричиняє зсув розкладки» на розробника заводити не можна: у переліку опиняються ті, кого посунули, а не той, хто посунув. Зібрати запис зсувів можна прямо на сторінці — але читати їх треба правильно.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue; // зсув одразу після дії — очікуваний
console.log('score', entry.value, 'зсунуто:', entry.sources.map((s) => s.node));
}
}).observe({ type: 'layout-shift', buffered: true });
Що дивитися і чому:
sources— це список постраждалих. Якщо зверху довантажився банер, у списку буде все, що під ним: заголовок, картки, кнопка. Причину шукають вище за потоком — у тому, що зʼявилося або змінило розмір безпосередньо перед зсувом.- Часові мітки важливіші за самі елементи. Порівняйте момент зсуву з таймлайном мережі: відповідь API, яка приїхала за 40 мс до стрибка, і є кандидатом у винуватці. Так само працює момент застосування шрифту або поява рекламного слота.
- Перевірте, чи зсув взагалі мав рахуватися. Зсуви протягом 500 мс після дискретного вводу помічені прапорцем і зазвичай виключаються — але скрол і зум таким вводом не вважаються, тож «та це ж користувач сам гортав» аргументом не є.
- Не дивуйтеся, що локально не відтворюється. У розробника картинки вже в кеші, а API відповідає за одиниці мілісекунд — стрибати просто нічому. Відтворення потребує холодного кешу й повільної мережі, інакше баг закриють як «не підтверджено».
- Не сумуйте зсуви руками. Метрика бере найбільшу серію зсувів, а не їхню суму, тож три окремі стрибки в різних кінцях сесії не складаються в одне число — і «в нас же всього 0,05 на кадр» не є захистом.
Межа бекенда і клієнта
- Можу пояснити, чому зелений серверний звіт не закриває питання швидкості: (time to first byte) — це підлога клієнтських метрик, а і містять у собі ще й редиректи, вивантаження попередньої сторінки й встановлення зʼєднання.
- Розумію, чому
loadіDOMContentLoaded— погані метрики «швидкості»: вони не привʼязані до того, що людина бачить на екрані.
Core Web Vitals: набір, пороги, зріз
- Називаю три метрики з тим, що кожна міряє: LCP — появу основного вмісту, — на дії, — стабільність розкладки.
- Памʼятаю обидві межі кожної метрики, а не лише «good» — 2500/4000 мс, 200/500 мс, 0,1/0,25 — і знаю, що нижня межа включна, а між межами лежить «needs improvement».
- Можу пояснити правило зрізу: 75-й , окремо мобільні й десктопні, усі три метрики виконано одночасно — і чому середнє тут не працює взагалі.
LCP
- Знаю, що LCP-кандидати — закритий перелік класів елементів, рахується лише видима у viewport частина, а в аналітику йде останній запис, бо звання найбільшого знімає тільки поява більшого елемента, а не видалення з DOM.
- Називаю чотири речі, що ламають вимір у тесті: перша ж взаємодія зупиняє алгоритм, фонова вкладка мовчить, SPA і bfcache метрику не скидають, зображення без
Timing-Allow-Originспотворює час рендерингу.
INP
- Можу розкласти взаємодії на три частини — очікування головного потоку, робота обробників, час до наступного кадру — і знаю, що відповіддю рахується саме кадр, а не завершення мережевих наслідків.
- Розумію, як із багатьох взаємодій виходить одне число: найгірша з поправкою «одна відкинута на кожні 50», зафіксована в момент виходу зі сторінки.
- Можу пояснити, чому синтетичний клік метрики не дає: події з
isTrusted === falseвимір не бачить у принципі, а зараховуються лише клік мишею, тап і натискання клавіші.
CLS
- Не плутаю чинне означення зі старим: CLS — найбільша серія зсувів (проміжок до 1 с, вікно до 5 с), а не сума всіх зсувів за сесію.
- Знаю формулу одного зсуву (частка впливу × частка відстані) і вікно
hadRecentInput— 500 мс після дискретного вводу, куди скрол і зум не входять. - Читаю
sourcesправильно: там зсунуті елементи, а не винуватець; при цьому памʼятаю, що метрика збирається лише в Chromium, а на сторінці зiframeвиходить фактично DCLS.
FCP, довгі задачі й TBT
- Можу пояснити різницю питань: FCP — «чи бодай щось почалося», LCP — «чи вже на екрані те, по що людина прийшла», і чому сплеш-екран закриває лише перше.
- Знаю, звідки поріг в 50 мс (бюджет RAIL на реакцію, поділений навпіл), що сумує лише надлишок понад цей поріг і чому він лишається лабораторним INP, а не заміною.
Лабораторія, поле й бал Lighthouse
- Можу пояснити розбіжність Lighthouse і Search Console без слова «баг»: одне — керована лабораторія, друге — 75-й перцентиль поля за 28-денне вікно, і пріоритети ставлять за .
- Розумію природу балу Lighthouse: це зважене середнє за позицією в логнормальному розподілі HTTP Archive, тож бали різних версій незіставні, а стрибки дають умови вимірювання, а не інструмент.
Виміри в коді й в автотестах
- Знаю, які факти з таймлайна детерміновані й придатні для e2e (редиректи, тип навігації, вага ресурсів, блокування рендерингу, стиснення, влучання в кеш) і три пастки поруч: зарубаний ресурс у таймлайн не додається, крос-оріджин запис маскується до самої тривалості, буфер типово тримає 250 записів.
- Розумію, чому для вимірів беруть
performance.now(), а неDate.now(), і чому знятий записPerformanceObserverще не дорівнює метриці — через фонову вкладку, bfcache, крос-оріджин фрейми й відлік відactivationStart.
SEO
- Формулюю обережно й правильно: у ранжуванні використовуються й дані для цього беруться з , але зелений звіт високих позицій не гарантує, а порожньо в CrUX — це відсутність вибірки, не зелений результат.
Квіз
Перед стартом
- Питань: 17
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Яку роль відіграє TTFB у клієнтських метриках?