Продуктивність фронтенда: Core Web Vitals без міфів — що реально впливає

Було корисно?

Ви випустили редизайн. Продукт подобається команді. Потім панелі моніторингу спалаху <- Core Web Vitals «Потребує покращення», конверсія впала, а служба підтримки пише, що «все затримується», ніби це одна помилка, яку можна знайти grep.

Саме тут команди марнують тижні, поліруючи не ту дрібницю. Core Web Vitals — вимірювані метрики, але вони не чарівні. Ставтеся до них як до будь-якого SLO у продакшені: зрозумійте, що насправді ламається, ізолюйте вузьке місце і випустіть найменше виправлення, яке змінює криву.

Core Web Vitals простими словами (без містики)

Core Web Vitals (CWV) — невеликий набір метрик, орієнтованих на користувача. Це не «секретний соус Google». Це стандартний спосіб описати: (1) як швидко зʼявляється значущий контент, (2) наскільки стабільно сторінка рендериться під час завантаження, і (3) наскільки відчутною є відповідь інтерфейсу, коли користувач щось робить.

LCP: Largest Contentful Paint (завантаження)

LCP відповідає на питання: «Коли користувач побачив головний елемент?» Зазвичай це герой-зображення, великий заголовок або верхній блок контенту. Це не «коли зʼявився спіннер». Це не «DOMContentLoaded». Це найбільший видимий елемент у вікні перегляду.

Типові фактори, що вбивають LCP:

  • Високий TTFB (повільний origin, промахи кеша, затратний SSR, погана конфігурація CDN)
  • CSS, що блокує рендер
  • Герой-зображення без пріоритету (lazy-loaded вище згину, без preload, великий розмір у байтах)
  • Клієнтське рендерення, яке відкладає контент до виконання JS
  • Сторонні скрипти, що відбирають main thread на початку

CLS: Cumulative Layout Shift (візуальна стабільність)

CLS відповідає на питання: «Чи рухалася сторінка під курсором користувача?» Зміщення макета — ті дратівливі стрибки, коли зображення, реклама або пізно завантажені шрифти штовхають контент. CLS не про анімацію; він про несподівані переміщення.

Типові фактори, що вбивають CLS:

  • Зображення/iframe без width/height (або без aspect-ratio)
  • Вставлені банери (cookie consent, промо-стрічки), що штовхають контент
  • Пізні font swap’и, що змінюють розкладку тексту
  • Рекламні виджети, які змінюють розміри після завантаження

INP: Interaction to Next Paint (відповідальність)

INP відповідає на питання: «Коли користувач взаємодіє, скільки часу проходить до оновлення UI?» Він замінив FID, бо користувачі не турбуються лише про першу взаємодію; їх цікавить, наскільки сайт відчутний протягом сеансу. INP чутливий до довгих задач, конкуренції за main-thread та дорогих обробників подій.

Операційний переклад: LCP — це здебільшого проблема каналу доставки (мережа + критичні ресурси). CLS — це здебільшого дисципліна верстки. INP — це здебільшого планування CPU та чистота коду.

Одна цитата, бо вона підходить і до веб-перформансу: Парафраз ідеї Джона Оустерхута: складність — корінь багатьох проблем у програмному забезпеченні; зменшіть її, щоб покращити надійність і продуктивність.

Жарт №1: Робота з продуктивністю схожа на дієту: люди вірять у дивні трюки, але «їж менше калорій» все одно перемагає.

Що справді рухає показники: стек пріоритетів

Більшість команд марнують час, бо ставляться до CWV як до чекліста мікрооптимізацій. Не так. Ставтеся до цього як до реагування на інцидент: визначте домінуюче обмеження, виправте його, переміряйте, повторіть.

1) Виправте критичний шлях, перш ніж лізти в мікрооптимізації

Якщо HTML доходить 800ms, зрізати 20ms з бандла JS — це театральність. Ваш критичний шлях:

  1. DNS/TCP/TLS (іноді приховано CDN, іноді ні)
  2. HTML TTFB + розмір HTML
  3. Критичний CSS і ресурси, що блокують рендер
  4. Герой-зображення (пріоритет + байти)
  5. Гідратація / виконання JS (для SPA/SSR гібридів)

Посуньте LCP, виправивши найраніше обмеження, яке повільне.

2) Кешуйте серйозно (і перевіряйте це)

«Ми використовуємо CDN» ≠ «наш HTML і герой-активи реально подаються з кеша для користувачів». CWV орієнтовані на користувача; якщо 40% користувачів промахуються кеша через кукі, Vary або гео-поведінку, ваші показники будуть погані.

3) Припиніть lazy-loading вище згину

Lazy-loading добре для контенту нижче згину. Вище згину часто це самосаботаж: ви кажете браузеру «це не важливо», а потім дивуєтеся, чому LCP страждає.

4) Робіть макет стабільним за дизайном

Виправлення CLS часто нудні: оголосіть розміри, зарезервуйте простір, не вставляйте елементи, що змінюють макет пізно. Це довготривалий виграш, який не залежить від пристрою, мережі чи удачі.

5) Знижуйте борг main-thread

INP — ваш рахунок за «JavaScript як податок». Ви платите його довгими задачами, важкими фреймворками та сторонніми скриптами. Виправлення — це не один трюк; це дисципліна: менші бандли, менше observers, менше перерендерів, розумніше планування.

6) Сторонні скрипти: ставтеся до них як до продакшен-залежностей

Маркетингові теги, A/B тестування, чат-виджети, fraud-detection: вони працюють на CPU ваших користувачів, а не на вашому. Вони можуть домінувати над INP і навіть шкодити LCP, якщо виконуються рано. Завантажуйте їх пізніше, ізолюйте, або видаляйте. Так, видаляйте.

Жарт №2: Найшвидший сторонній скрипт — той, який ваша юридична служба вже погодилася видалити.

Факти й історія, які пояснюють сучасний безлад

  • Факт 1: «DOMContentLoaded» і «onload» стали популярні, бо їх легко вимірювати, а не тому, що вони відображали сприйняття користувача.
  • Факт 2: HTTP/2 змінив компроміс «один великий бандл проти багатьох дрібних» завдяки мультиплексуванню запитів, але head-of-line blocking не зник всюди; транспорт все ще має значення.
  • Факт 3: Широке використання SPA змістило проблеми продуктивності з мережевих на CPU-залежні: користувачі чекають парсингу/виконання JS, а не тільки байтів.
  • Факт 4: Веб-шрифти колись були простим візуальним доповненням; тепер вони ризик для продуктивності, бо впливають на рендер і можуть викликати зсуви макета при swap.
  • Факт 5: Ера «lazy-load усього» була реакцією на важкі сторінки, але вона створила новий клас багів: відкладення того самого контенту, за яким прийшов користувач.
  • Факт 6: Google представив Web Vitals як зусилля до стандартизованих, орієнтованих на користувача метрик; у індустрії було надто багато несумісних визначень «швидко».
  • Факт 7: INP замінив FID, бо оптимізація лише першої взаємодії дозволяла сторінкам «впоратися», але вони все ще підлагували під час реального використання.
  • Факт 8: Нестабільність макета посилилася, коли ad-tech екосистема нормалізувала динамічну вставку контенту; CLS — це фактично метрикафікація користувацького гніву.
  • Факт 9: RUM (real user monitoring) став критичним, бо лабораторні тести не можуть змоделювати всі пристрої, мережі, CPU-throttle чи розширення середовища.

Швидкий план діагностики (перший/другий/третій)

Це найшвидший шлях до вузького місця, коли сторінка провалює CWV. Мета: не вирішувати все одразу. Знайдіть домінуючий обмежувач.

Перший: вирішіть, чи це LCP, CLS чи INP (і для яких сторінок)

  • Використовуйте RUM, щоб визначити URL-и/шаблони з найгіршими показниками (не тільки середні).
  • Сегментуйте за класом пристрою (мобільні зазвичай першими показують правду).
  • Дивіться на p75, а не на середнє. CWV оцінюється за перцентилями.

Другий: для LCP розділіть на сервер/клієнт/байти

  1. TTFB високий? Виправте кешування, латентність origin, вартість SSR, конфігурацію edge.
  2. TTFB нормальний, але LCP високий? Подивіться на CSS, що блокує рендер, пріоритет/розмір героя та preload-и.
  3. LCP-елемент — текст? Перегляньте поведінку завантаження шрифтів і блокування CSS.
  4. LCP-елемент — зображення? Виправте формат/розмір, підказки пріоритету, кешування та уникайте пізнього декодування.

Третій: для INP знайдіть довгі задачі та винних обробників

  • Використовуйте трасування, щоб знайти довгі задачі > 50ms; шукайте повторення.
  • Визначте важкі обробники подій (click/input/keydown) та шторм перерендерів.
  • Аудитуйте сторонні скрипти й таймери; виміряйте їхній main-thread час.

Тріаж CLS: слідкуйте за пізніми вставками і відсутніми розмірами

  • Знайдіть основні джерела зсувів; зазвичай це невеликий набір елементів.
  • Виправте, зарезервувавши простір і уникаючи вставок, що впливають на макет вище згину.

Якщо ви не можете відповісти на питання «який LCP-елемент для цього шаблону?» за 10 хвилин, ви не займаєтеся інженерією продуктивності. Ви займаєтеся враженнями.

Практичні завдання: команди, виводи, рішення

Це нудні, виконувані завдання, які можна зробити сьогодні. Кожне має: команду, приклад виводу, що це означає, і рішення. Вам не потрібні всі щодня; потрібні правильні, коли застрягли.

Завдання 1: Виміряти TTFB і кешування на edge за допомогою curl

cr0x@server:~$ curl -s -o /dev/null -D - https://www.example.com/ | egrep -i 'HTTP/|cache-control|age|x-cache|server-timing|vary'
HTTP/2 200
cache-control: public, max-age=0, s-maxage=600
age: 512
x-cache: HIT
server-timing: cdn-cache;desc=HIT, edge;dur=12, origin;dur=0
vary: Accept-Encoding

Що це означає: age: 512 і x-cache: HIT свідчать, що відповідь подається з кеша. server-timing вказує на відсутність часу origin.

Рішення: Якщо ви бачите MISS для реального трафіку, виправте ключі кеша (куки, Vary, query params) і TTL на edge перед тим, як торкатися JS.

Завдання 2: Перевірити час до першого байту точно з curl timings

cr0x@server:~$ curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://www.example.com/
dns=0.012 connect=0.045 tls=0.089 ttfb=0.312 total=0.428

Що це означає: TTFB — 312ms. Весь запит — 428ms. Мережа не є основним злочинцем тут.

Рішення: Якщо TTFB > ~800ms на кеш-хітах, ймовірно у вас латентність edge/origin або накладні витрати на генерацію HTML. Виправляйте це перш за все.

Завдання 3: Виявити ресурси, що блокують рендер, за допомогою Lighthouse CI (headless)

cr0x@server:~$ npx lighthouse https://www.example.com/ --quiet --chrome-flags="--headless" --only-categories=performance --output=json --output-path=./lh.json
...Saved JSON report to ./lh.json...
cr0x@server:~$ jq '.audits["render-blocking-resources"].details.items[] | {url, totalBytes, wastedMs}' lh.json | head
{
  "url": "https://www.example.com/assets/app.css",
  "totalBytes: 184322,
  "wastedMs": 410
}
{
  "url": "https://www.example.com/assets/vendor.js",
  "totalBytes": 912443,
  "wastedMs": 280
}

Що це означає: CSS великий і блокує; vendor JS теж блокує (ймовірно через синхронні скрипти або неправильне використання preload).

Рішення: Inline критичний CSS, розділіть некритичний CSS і переконайтеся, що JS відкладено/асинхронно. Не «мінімізуйте сильніше» і вважайте роботу виконаною.

Завдання 4: Підтвердити LCP-елемент і його ланцюжок запитів (trace через Chrome DevTools Protocol)

cr0x@server:~$ npx chrome-har-capturer --url https://www.example.com/ --output ./page.har
Saved HAR to ./page.har
cr0x@server:~$ jq '.log.entries[] | select(.response.content.mimeType|test("image|text/html|text/css")) | {url: .request.url, status: .response.status, size: .response.content.size, wait: .timings.wait}' page.har | head
{
  "url": "https://www.example.com/",
  "status: 200,
  "size: 62310,
  "wait: 180
}
{
  "url": "https://www.example.com/assets/app.css",
  "status: 200,
  "size: 184322,
  "wait: 92
}
{
  "url": "https://www.example.com/images/hero.jpg",
  "status: 200,
  "size: 1452200,
  "wait: 210
}

Що це означає: Герой-зображення 1.45MB і чекало 210ms перед першим байтом. Класичний LCP-анкгер.

Рішення: Конвертуйте герой у AVIF/WebP, змініть розміри, подавайте адаптивні варіанти і забезпечте раннє запитування (без lazy-load, розгляньте preload).

Завдання 5: Перевірити кешованість і стиснення герой-зображення

cr0x@server:~$ curl -s -I https://www.example.com/images/hero.jpg | egrep -i 'content-type|content-length|cache-control|etag|accept-ranges|content-encoding'
content-type: image/jpeg
content-length: 1452200
cache-control: public, max-age=3600
etag: "a9d1-5f2c9d3f"
accept-ranges: bytes

Що це означає: Це JPEG, великий, і кешується годину. Кешування ок, кодування — ні.

Рішення: Впровадьте сучасні формати і менші розміри. Кеш не допоможе першому відвідувачу.

Завдання 6: Перевірити, чи HTML випадково не некешований через куки/Vary

cr0x@server:~$ curl -s -I https://www.example.com/ | egrep -i 'set-cookie|vary|cache-control'
cache-control: private, no-store
set-cookie: session=...; Path=/; Secure; HttpOnly
vary: Cookie

Що це означає: Ви сказали всім кешам відійти убік. Це податок TTFB для кожного користувача.

Рішення: Відокремте персоналізований контент від кешованого shell. Уникайте Vary: Cookie на HTML, якщо ви не готові за це платити.

Завдання 7: Знайти довгі задачі в локальному трасі, захопленому Chromium

cr0x@server:~$ chromium --headless --disable-gpu --trace-startup --trace-startup-file=./trace.json https://www.example.com/
[0204/090312.112233:INFO:headless_shell.cc(661)] Written trace file to ./trace.json
cr0x@server:~$ jq '[.. | objects | select(has("dur") and has("name")) | select(.dur > 50000) | {name, dur, cat}] | sort_by(.dur) | reverse | .[0:5]' trace.json
[
  {
    "name": "EvaluateScript",
    "dur": 182334,
    "cat": "devtools.timeline"
  },
  {
    "name": "FunctionCall",
    "dur": 93422,
    "cat": "devtools.timeline"
  }
]

Що це означає: У вас є довгі задачі >50ms, особливо оцінка скриптів. Це територія INP.

Рішення: Зменшіть JS, що відвантажується/виконується рано. Розділіть бандли, видаліть мертвий код і відкладіть некритичні сторонні скрипти.

Завдання 8: Квантифікувати байти JS/CSS за маршрутом використовуючи артефакти збірки

cr0x@server:~$ ls -lh dist/assets | egrep '\.js$|\.css$' | head
-rw-r--r-- 1 cr0x cr0x  912K Feb  4 09:01 vendor-9a12c.js
-rw-r--r-- 1 cr0x cr0x  286K Feb  4 09:01 app-1b22f.js
-rw-r--r-- 1 cr0x cr0x  181K Feb  4 09:01 app-4aa2.css

Що це означає: Vendor-чанк величезний. Це часто корелює з часом парсингу/компіляції на телефонах середнього класу.

Рішення: Проведіть аудит залежностей. Якщо ви відправляєте три бібліотеки для дат і два менеджери стану, оберіть один і видаліть решту.

Завдання 9: Виявити невикористаний CSS на сторінці (швидко через coverage в Puppeteer)

cr0x@server:~$ node -e '
const puppeteer=require("puppeteer");
(async()=>{
  const b=await puppeteer.launch({headless:"new"});
  const p=await b.newPage();
  await p.coverage.startCSSCoverage();
  await p.goto("https://www.example.com/",{waitUntil:"networkidle2"});
  const cov=await p.coverage.stopCSSCoverage();
  let used=0,total=0;
  for (const c of cov){ total+=c.text.length; used+=c.ranges.reduce((s,r)=>s+(r.end-r.start),0); }
  console.log(`css_used=${(used/1024).toFixed(1)}KB css_total=${(total/1024).toFixed(1)}KB used_pct=${(used/total*100).toFixed(1)}%`);
  await b.close();
})();'
css_used=28.4KB css_total=412.7KB used_pct=6.9%

Що це означає: Ви відправляєте зимове пальто на пляж. 93% CSS невикористані для цього маршруту.

Рішення: Розділяйте CSS за маршрутами, очищуйте невикористані стилі і уникайте глобальних фреймворків, що підвантажуються скрізь «на всяк випадок».

Завдання 10: Перевірити поведінку завантаження шрифтів і ризик CLS

cr0x@server:~$ curl -s -I https://www.example.com/assets/fonts/brand.woff2 | egrep -i 'content-type|cache-control|timing-allow-origin'
content-type: font/woff2
cache-control: public, max-age=31536000, immutable
timing-allow-origin: *

Що це означає: Шрифт дружній до кешу і відкриває дані timing. Добра гігієна.

Рішення: Якщо CLS все ще високий, перевірте відсутність font-display або невідповідність fallback-метрик; розгляньте стек системних шрифтів для критичного тексту.

Завдання 11: Знайти зростання сторонніх скриптів у запитах

cr0x@server:~$ jq -r '.log.entries[].request.url' page.har | egrep -i 'goog|doubleclick|segment|mixpanel|hotjar|optimizely|datadog|newrelic' | sort | uniq | head
https://cdn.segment.com/analytics.js/v1/...
https://www.googletagmanager.com/gtm.js?id=GTM-...

Що це означає: У вас є сторонні залежності, які можуть виконуватися рано і часто.

Рішення: Перенесіть їх за згодою користувача і/або після LCP. Якщо бізнес наполягає, принаймні зробіть їх неблокуючими і відкладіть ініціалізацію.

Завдання 12: Перевірити серверне стиснення і розмір HTML

cr0x@server:~$ curl -s -H 'Accept-Encoding: gzip, br' -I https://www.example.com/ | egrep -i 'content-encoding|content-type|content-length'
content-type: text/html; charset=utf-8
content-encoding: br
content-length: 24132

Що це означає: Brotli увімкнено і HTML ~24KB стиснений. Це нормально.

Рішення: Якщо стиснення відсутнє — увімкніть його на edge/origin; якщо HTML величезний — припиніть інлайнити дампи JSON стану в сторінку.

Завдання 13: Підтвердити, що CDN подає правильні варіанти зображень за Accept header

cr0x@server:~$ curl -s -I -H 'Accept: image/avif,image/webp,image/*,*/*;q=0.8' https://www.example.com/images/hero | egrep -i 'content-type|vary|cache-control'
content-type: image/avif
vary: Accept
cache-control: public, max-age=31536000, immutable

Що це означає: Ви подаєте AVIF, коли клієнт його підтримує, і правильно ставите Vary на Accept.

Рішення: Якщо ви завжди подаєте JPEG/PNG, ви платите податок за пропускну здатність, що прямо б’є по LCP на мобайлі.

Завдання 14: Перевірити випадковий no-cache на статичних ресурсах

cr0x@server:~$ curl -s -I https://www.example.com/assets/app-1b22f.js | egrep -i 'cache-control|etag'
cache-control: public, max-age=31536000, immutable
etag: "a1b2c3"

Що це означає: Добре: fingerprinted assets кешуються довго.

Рішення: Якщо ви бачите no-cache на fingerprinted ресурсах — виправте це негайно; ви змушуєте повторні завантаження і дарма спалюєте INP через додаткову роботу парсингу.

Три корпоративні міні-історії з поля бою продуктивності

Міні-історія 1: Інцидент через хибне припущення («CDN означає швидко»)

Команда споживчого додатка впровадила персоналізацію на лендингу. Це було акуратно: «Ласкаво просимо назад» і кілька рекомендацій. У них був CDN, тож вони припустили, що вплив мінімальний. Зміна пройшла unit-тести, e2e та синтетичну перевірку продуктивності в офісі.

Через два дні CWV звіт для мобайлу впав. Тікети в підтримці описували «білу сторінку» і «тап не відповідає». Продакт подумав, що це регресія JS. Фронтенд команда почала зменшувати бандли. Бекенд команда профілювала API. Всі були зайняті; ніхто не був ефективним.

На чергуванні SRE зробив нудну річ: curl -I на домашній сторінці і подивився заголовки. HTML тепер мав Cache-Control: private, no-store і Vary: Cookie. Код персоналізації торкнувся сесійного стану рано, через що фреймворк позначив відповідь як некешовану. CDN не був «повільним»; його обходили. Кожен запит йшов на origin, виконував SSR-роботу, і мобільні користувачі платили повну ціну.

Виправлення не було героїчним. Вони розділили сторінку на кешований shell і невеликий блок персоналізації, який отримується клієнтом після першого рендеру. Також обрізали куки, бо заголовки запитів ставали великими для повільних мереж.

Урок зрозумілий: припущення про кешування — не архітектура. Заголовки — архітектура. Перевіряйте поведінку кеша реальними запитами, а не відчуттями.

Міні-історія 2: Оптимізація, що відбилася боком (lazy-loading героя)

В B2B дашборді були хороші показники на десктопі, але поганий мобільний LCP на маркетингових сторінках. Команда вирішила «lazy-load більше зображень», бо це виглядало як легка перемога і веб повний порад від людей, які, мабуть, ніколи не запускали сайт з герой-банером.

Вони додали loading="lazy" на всі зображення глобально через компонент. У локальній розробці виглядало нормально. У стейджингу синтетичні тести показали менше байтів на початку. Зміна вийшла в прод.

За тиждень LCP погіршився. LCP-елементом був герой, і браузер тепер його депріоритезував. Запит на зображення почався пізніше, декодування — пізніше, і головний контент зʼявився пізніше з погляду користувача. Конверсія трохи впала — не досить для сигналів, але достатньо, щоб маркетинг почав зустрічі «сайт здається повільним».

Коли це простежили, все стало очевидно: герой був навмисно відкладений. Вони скасували lazy-loading для зображень вище згину, додали адаптивні джерела і вибірково використали preload. У підсумку — менше байтів і ранніший paint, бо пріоритетували потрібні байти.

Урок: браузер добре розподіляє пріоритети, коли йому не брешуть. Lazy-load — не чеснота; це інструмент. Використовуйте його там, де належить.

Міні-історія 3: Нудна, але правильна практика, що врятувала ситуацію (бюджети та канарки)

Фінтех-компанія мала культуру продуктивності без гламуру. У них були бюджети за маршрутом: максимум байтів JS, максимум байтів CSS і правило «жодного нового стороннього скрипта без ревʼю». Інженери іноді скаржилися. Потім вони забули про це — це найвища похвала для механізму контролю.

Одного кварталу ввели нового аналітичного вендора. Сніпет був малим, але він підхопив велику бібліотеку і почав робити важку роботу на взаємодіях. На стейджингу ніхто не помітив; трафік і пристрої стейджингу не були репрезентативні, а додаток і так «достатньо швидкий» в лабораторних тестах.

Пайплайн розгортання виконав канарку в проді з RUM-ворітами. Канарка показала регрес INP, сконцентровану на пристроях Android середнього класу і на маршруті checkout. Ролл-аут автоматично призупинився. Без драм, без звинувачень. У команди був чіткий диф: «INP ↑; новий сторонній скрипт ініціалізується до взаємодії».

Вони працювали з інтеграцією вендора: відклали ініціалізацію до простою сторінки і обмежили трекінг тільки маршрутами, де це дійсно важливо. Також помістили його за згодою. Канарка пройшла; розгортання відновили.

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

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

1) Симптом: LCP поганий лише при першому візиті

Корінна причина: Ресурси кешуються добре, але критичне герой-зображення занадто велике або не в сучасних форматах; перші відвідувачі платять повний кошт скачування/декодингу.

Виправлення: Використовуйте адаптивні зображення і AVIF/WebP; забезпечте ранній запит героя; перевірте заголовки кеша і стиснення CDN.

2) Симптом: LCP поганий і TTFB теж високий

Корінна причина: Латентність origin, повільний SSR, обхід кеша через куки або персоналізацію, або неправильна конфігурація CDN.

Виправлення: Зробіть HTML кешованим; перемістіть персоналізацію в edge-includes або клієнтський fetch після paint; профілюйте SSR; виправте ключі кеша; зменшіть Vary і розмір куків.

3) Симптом: CLS стрибає на сторінках з рекламою або банерами згоди

Корінна причина: Пізні вставки в DOM вище згину; слоти реклами змінюють розміри після завантаження; банер штовхає контент вниз.

Виправлення: Зарезервуйте простір (фіксовані контейнери або min-height), рендеріть заповнювачі, уникайте вставок вище згину після першого paint.

4) Симптом: CLS малий в лабораторії, але високий у RUM

Корінна причина: Варіативність реальних користувачів: різні розміри вікон впливають на перенос рядків; шрифти змінюються по-різному; сторонні віджети поводяться непослідовно.

Виправлення: Використовуйте RUM, щоб знайти джерела зсувів; тестуйте з різними viewports; застосуйте явні розміри і стабільні fallback-шрифти; обмежте контейнери сторонніх елементів.

5) Симптом: INP поганий на «простих» сторінках

Корінна причина: Сторонні скрипти або глобальні слухачі виконують роботу на кожну взаємодію; важка гідратація; довгі задачі від runtime фреймворка.

Виправлення: Відкладіть ініціалізацію сторонніх; видаліть непотрібні слухачі; розділіть гідратацію; перемістіть некритичну роботу в idle callbacks; зменшіть обʼєм JS.

6) Симптом: Зменшення розміру бандла не покращило INP

Корінна причина: Проблема не в розмірі завантаження; а в патернах виконання (перерендери, трясіння макета, важкі обробники) або в одному дорогому шляху взаємодії.

Виправлення: Трасуйте взаємодії; знайдіть довгі задачі; виправте шторм перерендерів; зменшіть синхронну роботу в обробниках; використовуйте батчинг і мемоізацію обережно.

7) Симптом: LCP погіршується після додання «preload для всього»

Корінна причина: Інверсія пріоритетів: preloading занадто багатьох ресурсів конкурує з дійсним LCP-ресурсом.

Виправлення: Preload лише справді критичні ресурси (зазвичай один герой, можливо один шрифт). Перевіряйте пріоритет мережі в трасах.

8) Симптом: Мобільні значно гірші за десктоп по всіх метриках

Корінна причина: JS, що навантажує CPU, та важка робота з макетом; десктоп приховує це сирою силою.

Виправлення: Тестуйте з CPU-throttling і профілями середніх пристроїв; зменшуйте виконання JS; розбивайте довгі задачі; спрощуйте UI і DOM.

Чеклісти / покроковий план

Покроковий план для рятування LCP (по одному шаблону)

  1. Визначте LCP-елемент з RUM (або trace) для цього шаблону. Якщо не можете його назвати — зупиніться і знайдіть.
  2. Перевірте TTFB на cache hit і miss. Якщо кеш-хіт повільний — виправляйте edge/origin перед фронтенд-роботою.
  3. Аудит байтів героя: формат, розміри, стиснення і заголовки кеша.
  4. Забезпечте пріоритет: уникайте lazy-load вище згину; preload героя коли доречно; не топіть його в інших preload-ах.
  5. Зменшіть ресурси, що блокують: inline/розділ критичного CSS; відкладіть некритичний JS.
  6. Переміряйте з RUM на p75 мобільних. Випустіть і верифікуйте. Не зупиняйтесь на «в лабораторії краще».

Покроковий план для стабілізації CLS

  1. Перелічіть топ елементів, що зсуваються за допомогою RUM або DevTools «Layout Shift Regions».
  2. Зарезервуйте простір для зображень/iframe/реклами через width/height або aspect-ratio.
  3. Припиніть пізні вставки вище згину або рендеріть їх у зарезервованому слоті від початку.
  4. Виправте шрифти: забезпечте розумні fallback-метрики; використайте font-display, що не викликає несподіваних перерахунків.
  5. Перевірте на різних viewports, бо перенос рядків може створювати CLS лише на певних екранах.

Покроковий план для довготривалого покращення INP

  1. Трасуйте погану взаємодію (click/input) на профілі середнього пристрою і знайдіть найдовші задачі.
  2. Визначте власника: ваш код vs сторонні скрипти vs runtime фреймворка.
  3. Розбийте довгі задачі і перемістіть роботу поза критичним шляхом взаємодії.
  4. Зменшіть перерендери: уникайте оновлень стану, що тригерять великі дерева компонентів; віртуалізуйте великі списки.
  5. Відкладіть некритичні скрипти і припиніть синхронну аналітику на подіях введення.
  6. Встановіть бюджет на довгі задачі і на розмір бандла за маршрутом, а потім контролюйте це в CI/канарці.

Операційний чекліст: зберегти досягнення від регресій

  • RUM-дашборди за шаблонами і класом пристрою з алертами на p75 регресії.
  • Бюджети продуктивності для JS/CSS байтів і додавання сторонніх скриптів.
  • Канарні релізи з автоматичним ролбеком/пауза при CWV-регресії.
  • Регулярне скорочення залежностей (квартально — нормально; щотижня — утопія).
  • Тести заголовків кеша в CI для критичних маршрутів і активів.

Питання й відповіді

1) Оптимізувати лабораторні показники чи RUM?

RUM — за правдою, лабораторія — для налагодження. Лабораторні тести контрольовані; вони чудові для виявлення очевидних регресій і ізоляції причин. RUM показує, що насправді відчувають клієнти, включно з повільними пристроями і дивними мережами.

2) Чому мій Lighthouse-скор змінюється кожного запуску?

Тому що продуктивність — розподілена система: jitter мережі, планувальник CPU, стан кеша і поведінка сторонніх компонентів змінюються. Використовуйте кілька запусків, дивіться на розподіли і фокусуйтеся на повторюваних вузьких місцях: великі герої, блокуючий CSS і довгі задачі.

3) Чи SPA за своєю суттю гірші для CWV?

Не за своєю суттю, але їх легше зіпсувати. SPA часто відкладають значущий контент до виконання JS, що шкодить LCP і може погіршувати INP. SSR/streaming і селективна гідратація можуть зменшити розрив, але тільки якщо критичний шлях лишається легким.

4) Чи треба інлайнити весь CSS?

Ні. Inline критичний CSS для вище-згинного контенту, розділіть решту і уникайте відправлення всієї системи дизайну на кожен маршрут. Інлайн всього може роздути HTML і затримати перший байт на повільних зʼєднаннях.

5) Чи preload завжди корисний?

Ні. Preload тільки один-два ресурси, що справді визначають LCP (зазвичай герой і, можливо, шрифт). Надмірний preload конкурує за пропускну здатність і може затримати справжній критичний ресурс.

6) Як шрифти впливають на CWV?

Шрифти можуть затримувати рендер тексту (погіршення сприйняття завантаження) і викликати зсуви макета при swap (погано для CLS). Кешуйте шрифти агресивно, обмежуйте варіанти і забезпечуйте fallback-метрики, що не викликають сильних перерахунків.

7) Який найшвидший «великий виграш» для CLS?

Дайте всьому розміри. Зображення, iframe, слоти реклами. Зарезервуйте простір для банерів. Найшвидші CLS-виправлення виглядають як хатня робота, бо такими і є.

8) Який найшвидший «великий виграш» для INP?

Видаліть або відкладіть сторонні скрипти, що виконуються при взаємодії, і розбийте довгі задачі. Також: не робіть важку роботу синхронно в обробниках input. Якщо аналітика потрібна, буферизуйте її.

9) Чому мобільні виглядають непропорційно гіршими?

Тому що мобільні CPU і радіо повільніші, і тиск памʼяті змінює все. Десктоп приховує багато гріхів. Якщо ви тестуєте лише на робочому ноуті — ви бенчмаркуєте неправильну машину.

10) Якщо мій бекенд швидкий, чи можна ігнорувати TTFB?

Ні. TTFB включає мережу, TLS, поведінку CDN, edge routing і промахи кеша. Бекенд може бути швидким, але користувачі все одно чекають, якщо ви випадково зробили HTML некешованим або погано сконфігурували маршрутизацію трафіку.

Наступні кроки, які можна випустити цього тижня

Не починайте з «оптимізувати все». Почніть з одного проблемного шаблону і здобудьте вимірюваний виграш на p75 мобільних.

  1. Оберіть найгірший високонавантажений шаблон з RUM (не головну сторінку з традиції).
  2. Пройдіть швидкий план діагностики і визначте, чи домінує TTFB, блокування рендеру, байти героя або довгі задачі.
  3. Впровадьте одне LCP-виправлення: зробіть HTML кешованим, пріоритетизуйте героя або зменшіть байти героя. Перевіряйте через заголовки і RUM.
  4. Впровадьте одне CLS-виправлення: зарезервуйте простір для топ-джерела зсувів. Перевіряйте CLS у RUM.
  5. Впровадьте одне INP-виправлення: видаліть/відкладіть один сторонній скрипт або розбийте один шлях довгої задачі. Перевірте покращення латентності взаємодії.
  6. Додайте запобіжники: бюджети, gating канарок і щотижневий огляд залежностей «що ми додали?».

Чесна таємниця: найкраща робота з CWV виглядає як робота з надійністю. Вимірюйте, ізолюйте, виправляйте, перевіряйте і запобігайтe регресіям. Якщо ви робите хитрі трюки без кривої «до/після», ви не налаштовуєте продуктивність — ви збираєте талісмани.

← Попередня
Зсув часу в WSL2: виправте неточність годинника правильно
Наступна →
Атаки DMA 101: як IOMMU не дає PCIe-пристрою заволодіти вашою оперативною пам’яттю

Залишити коментар