Чому CLS — найбільш недооцінена метрика Core Web Vitals
Якщо ви вже знайомі з оптимізацією LCP та INP, то розумієте: перша відповідає за швидкість завантаження, друга — за чуйність при взаємодії. Але є ще третя метрика, яку багато хто просто ігнорує, — Cumulative Layout Shift (CLS). Вона вимірює візуальну стабільність: наскільки контент «стрибає» під час завантаження й використання сторінки.
І ось тут починається цікаве.
За даними Web Almanac 2025, 72% сайтів проходять поріг CLS. Звучить непогано, так? Але зворотний бік цієї цифри — 28% сайтів, майже кожен третій, провалюють метрику. І наслідки бувають доволі драматичні. Rakuten 24 (великий японський e-commerce) провів детальне дослідження і виявив: сторінки з хорошим CLS давали на 53% вищий дохід на відвідувача, на 33% вищу конверсію та на 15% нижчий bounce rate. Це не просто «косметична» проблема — це реальні гроші.
А тепер подумайте: 62% мобільних сторінок мають хоча б одне зображення без заданих розмірів, 39% використовують анімації, що провокують зсуви, і лише 11% попередньо завантажують веб-шрифти. Більшість сайтів, чесно кажучи, сидять на пороховій бочці CLS-проблем — просто поки що їм щастить.
Отже, давайте розберемо CLS на складові, навчимося діагностувати проблеми сучасними інструментами DevTools і застосуємо конкретні техніки — від font metric overrides до bfcache — щоб зробити сайт по-справжньому стабільним.
Що таке CLS і як він обчислюється
Визначення метрики
CLS кількісно оцінює несподівані зсуви макета протягом усього життєвого циклу сторінки. На відміну від LCP та INP (які вимірюються в мілісекундах), CLS — безрозмірне число, засноване на площі зсуву та відстані переміщення елементів.
Порогові значення
- Добре: 0,1 або менше — користувач не помічає зсувів
- Потребує покращення: 0,1–0,25 — зсуви помітні та дратують
- Погано: понад 0,25 — контент стрибає, користувач випадково натискає не те
Ці пороги вимірюються на 75-му перцентилі — тобто 75% завантажень сторінки мають бути нижче 0,1, щоб отримати оцінку «добре».
Формула обчислення
Кожен окремий зсув макета оцінюється за двома компонентами:
- Impact Fraction (частка впливу) — яку частину viewport займає зона, уражена зсувом (об'єднання старої та нової позицій елемента)
- Distance Fraction (частка відстані) — на яку відстань елемент перемістився, поділену на найбільший розмір viewport
Layout Shift Score = Impact Fraction × Distance Fraction
Розглянемо приклад. Елемент висотою 50% viewport зсунувся на 25% вниз: Impact Fraction = 0,75 (50% + 25%), Distance Fraction = 0,25. Оцінка зсуву = 0,75 × 0,25 = 0,1875. Один-єдиний такий зсув — і ви вже за межами «добре». Вражає, правда?
Session Windows: як CLS агрегується
CLS не просто підсумовує всі зсуви за весь час — це було б несправедливо для довгих сесій. Натомість використовується концепція session windows (вікон сесій):
- Зсуви групуються у вікна: новий зсув додається до поточного вікна, якщо він відбувся менш ніж через 1 секунду після попереднього
- Максимальна тривалість вікна — 5 секунд
- CLS сторінки = оцінка найбільшого вікна (largest burst)
На практиці це означає, що один «залп» зсувів під час завантаження шрифту може бути набагато критичнішим, ніж кілька дрібних зсувів, розкиданих у часі.
Що не враховується: 500-мс grace period
Зсуви, що відбуваються протягом 500 мілісекунд після дії користувача (клік, тап, натискання клавіші), не враховуються в CLS. Тобто, якщо людина натискає кнопку «Показати більше» і контент розширюється — це очікуваний зсув, і CLS його ігнорує.
А ось якщо банер з'являється сам по собі через 3 секунди після завантаження — це вже зсув, і він буде зафіксований.
Діагностика проблем із CLS
Chrome DevTools: Layout Shift Culprits
У 2026 році Chrome DevTools отримав дуже зручний інструмент — Layout Shift Culprits, вбудований у панель Insights. Він автоматично визначає кластери зсувів, показує «винуватців» і анотує запис Performance Panel візуальними позначками. Набагато краще, ніж шукати зсуви вручну (а я пам'ятаю ті часи, коли доводилося гортати довжелезний timeline).
Як цим користуватися:
- Відкрийте DevTools (F12) → вкладка Performance
- Увімкніть опцію Web Vitals
- Запишіть трейс при завантаженні сторінки
- У панелі Insights знайдіть секцію Layout Shift Culprits
- Кожен кластер покаже елементи, що зсунулися, причину та амплітуду зсуву
Layout Shift Regions
Для швидкого візуального огляду є ще один прийом — увімкніть підсвічування зсувів: DevTools → Settings → More Tools → Rendering → Layout Shift Regions. Області зсувів підсвітяться фіолетовим кольором у реальному часі. Просто перезавантажте сторінку й одразу побачите, де «стрибає». Елементарно, але працює чудово.
Layout Instability API
Для програмного моніторингу використовуйте PerformanceObserver із типом layout-shift:
// Програмне відстеження зсувів макета
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Ігноруємо зсуви після взаємодії користувача
if (!entry.hadRecentInput) {
console.log('Layout shift:', {
value: entry.value.toFixed(4),
startTime: entry.startTime.toFixed(0),
sources: entry.sources?.map(s => ({
node: s.node?.nodeName,
previousRect: s.previousRect,
currentRect: s.currentRect
}))
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
Властивість sources покаже конкретні DOM-елементи, що зсунулися, разом з їхніми попередніми та новими координатами. Дуже корисно для розуміння, який саме елемент «їздить» по сторінці.
Бібліотека web-vitals з атрибуцією
Для збору польових даних від реальних користувачів (RUM) використовуйте бібліотеку web-vitals:
import { onCLS } from 'web-vitals/attribution';
onCLS((metric) => {
const { value, attribution } = metric;
const {
largestShiftTarget,
largestShiftValue,
largestShiftTime,
largestShiftSources,
loadState
} = attribution;
console.group(`CLS: ${value.toFixed(4)} (${metric.rating})`);
console.log(`Найбільший зсув: ${largestShiftTarget}`);
console.log(`Оцінка зсуву: ${largestShiftValue.toFixed(4)}`);
console.log(`Час: ${largestShiftTime.toFixed(0)}ms`);
console.log(`Стан завантаження: ${loadState}`);
console.groupEnd();
// Відправте в аналітику
sendToAnalytics({
metric: 'CLS',
value,
rating: metric.rating,
largestShiftTarget,
largestShiftValue,
loadState
});
});
Топ-причини CLS та їх вирішення
1. Зображення і відео без розмірів
Це причина номер один зсувів макета у світі. Коли браузер зустрічає <img> без атрибутів width і height, він виділяє під елемент нуль пікселів. А щойно зображення завантажиться — весь контент нижче різко зміщується.
Рішення просте — завжди вказуйте розміри:
<!-- Погано — браузер не знає розмірів заздалегідь -->
<img src="/photo.jpg" alt="Фото">
<!-- Добре — браузер резервує місце до завантаження -->
<img
src="/photo.jpg"
alt="Фото"
width="800"
height="450"
>
<!-- Ще краще — responsive з aspect-ratio -->
<img
src="/photo.jpg"
alt="Фото"
width="800"
height="450"
style="width: 100%; height: auto;"
>
Сучасні браузери автоматично обчислюють aspect-ratio з атрибутів width/height, тому зображення залишається responsive, але місце резервується коректно. Для складніших випадків використовуйте CSS-властивість aspect-ratio напряму:
.video-container {
aspect-ratio: 16 / 9;
width: 100%;
background: #f0f0f0; /* Placeholder колір */
}
.hero-image {
aspect-ratio: 2 / 1;
width: 100%;
object-fit: cover;
}
2. Веб-шрифти: тихий вбивця CLS
Шрифти — це, мабуть, найпідступніша причина зсувів, бо їх важко помітити на око. Що відбувається: коли кастомний шрифт завантажується із затримкою, браузер спочатку показує текст fallback-шрифтом (скажімо, Arial). Щойно кастомний шрифт готовий — відбувається заміна, і через різницю в метриках весь текст перерозподіляється. Це так званий FOUT (Flash of Unstyled Text).
І лише 11% сторінок попередньо завантажують шрифти. Решта — вразливі.
Стратегія 1: font-display: optional (нульовий CLS)
Якщо продуктивність для вас — головний пріоритет, використовуйте font-display: optional. Браузер дає шрифту лише 100 мс на завантаження. Не встиг — залишається fallback, і заміни не відбувається взагалі. Нуль CLS, гарантовано:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-v13.woff2') format('woff2');
font-display: optional;
font-weight: 400;
font-style: normal;
}
Мінус: на повільних з'єднаннях користувач побачить fallback-шрифт замість кастомного. Для основного тексту це зазвичай цілком прийнятно.
Стратегія 2: font-display: swap + Font Metric Overrides (мінімальний CLS)
Якщо бренд вимагає, щоб кастомний шрифт завжди відображався, використовуйте swap у парі з font metric overrides. Це CSS-властивості, що підганяють розміри fallback-шрифту під кастомний:
/* Fallback-шрифт, налаштований під метрики Inter */
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
ascent-override: 90.20%;
descent-override: 22.48%;
line-gap-override: 0.00%;
size-adjust: 107.40%;
}
/* Основний шрифт */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-v13.woff2') format('woff2');
font-display: swap;
font-weight: 400;
}
body {
font-family: 'Inter', 'Inter-fallback', sans-serif;
}
Властивості ascent-override, descent-override, line-gap-override та size-adjust змушують fallback-шрифт займати рівно стільки ж місця, скільки кастомний. Коли відбувається заміна — зсув мінімальний або взагалі нульовий.
До речі, фреймворки Next.js і Nuxt вміють генерувати ці override автоматично. Для інших проєктів є бібліотеки Fontaine або Capsize.
Не забудьте preload
Незалежно від обраної стратегії font-display, завжди попередньо завантажуйте критичні шрифти:
<link
rel="preload"
href="/fonts/inter-v13.woff2"
as="font"
type="font/woff2"
crossorigin
>
3. Реклама, embed та iframe
Рекламні блоки — класичне джерело CLS. Рекламна мережа вставляє контент через JavaScript після завантаження сторінки, і якщо під рекламу не зарезервовано місце — весь контент нижче зсувається. Особливо болісно, коли реклама з'являється над основним контентом (і так, я бачив це на десятках реальних сайтів).
Рішення — завжди резервуйте фіксований простір для рекламних слотів:
/* Резервуємо місце під рекламний блок */
.ad-slot {
min-height: 250px;
width: 300px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.ad-slot::before {
content: 'Реклама';
color: #ccc;
font-size: 0.875rem;
}
/* Для responsive рекламних форматів */
.ad-slot-leaderboard {
min-height: 90px;
width: 100%;
max-width: 728px;
}
.ad-slot-rectangle {
min-height: 250px;
width: 300px;
}
Для iframe та embed (YouTube, карти) — теж резервуйте місце через aspect-ratio:
<div style="aspect-ratio: 16/9; width: 100%;">
<iframe
src="https://www.youtube.com/embed/..."
width="100%"
height="100%"
loading="lazy"
title="Відео"
></iframe>
</div>
4. Некомпозитні CSS-анімації
Це друга за поширеністю причина CLS після зображень без розмірів. 39% мобільних сторінок мають анімації, що провокують перерахунок макета. Проблема виникає, коли ви анімуєте властивості, що тригерять layout: width, height, top, left, margin, padding.
/* Погано — анімація тригерить layout на кожному кадрі */
@keyframes slideDown {
from { height: 0; }
to { height: 200px; }
}
.dropdown {
animation: slideDown 0.3s ease;
}
/* Добре — transform працює на GPU, не тригерить layout */
@keyframes slideDown {
from { transform: scaleY(0); }
to { transform: scaleY(1); }
}
.dropdown {
transform-origin: top;
animation: slideDown 0.3s ease;
}
/* Альтернатива — translate замість top/left */
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
.sidebar {
animation: slideIn 0.3s ease;
}
Правило просте: анімуйте тільки transform та opacity. Ці властивості обробляються GPU-композитором і ніколи не викликають layout. Запам'ятайте це як мантру.
5. Динамічний контент без резервації місця
Cookie-банери, промобари, банери з повідомленнями — усе це часто вставляється після завантаження й зсуває контент. Золоте правило: ніколи не вставляйте контент над існуючим, якщо це не реакція на дію користувача.
Якщо банер необхідний — використовуйте position: fixed або position: sticky, щоб він не зсував решту:
/* Cookie-банер, що не зсуває контент */
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
background: #333;
color: #fff;
padding: 1rem;
}
/* Або зарезервуйте місце заздалегідь */
.notification-area {
min-height: 48px; /* Висота банера */
contain: layout;
}
6. Каруселі та слайдери
Автоматичні каруселі можуть генерувати нескінченні зсуви макета, якщо використовують некомпозитні анімації. Ось що допоможе:
- Використовуйте
transform: translateX()замість зміниleftабоmargin-left - Задайте фіксовану висоту контейнера каруселі
- Або взагалі перемістіть карусель нижче за fold — зсуви поза viewport мають менший Impact Fraction
Просунуті техніки CLS-оптимізації
content-visibility + contain-intrinsic-size
CSS-властивість content-visibility: auto дозволяє браузеру пропускати рендеринг елементів поза viewport — це суттєво прискорює початкове завантаження. Але є нюанс: без contain-intrinsic-size це може спричинити зсуви, коли елемент з'являється у viewport:
/* Відкладений рендеринг без зсувів */
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
/* 'auto' дозволяє браузеру запам'ятати
реальний розмір після першого рендерингу */
}
/* Для коментарів або довгих списків */
.comments-section {
content-visibility: auto;
contain-intrinsic-size: auto 800px;
}
Ключове слово auto перед розміром дозволяє браузеру запам'ятати реальні розміри після першого рендерингу. При повторному скролінгі зсувів уже не буде.
bfcache: найбільший прорив для CLS
Back/forward cache (bfcache) — це, на мою думку, одна з найнедооціненіших оптимізацій. Суть проста: браузер зберігає повний знімок сторінки в пам'яті при навігації назад/вперед. Коли користувач повертається — сторінка відновлюється миттєво, без жодних зсувів. CLS такої навігації = 0.
Наскільки це важливо? Приблизно кожна п'ята мобільна навігація — це перехід назад/вперед. Команда Chrome називає bfcache найбільшим одноразовим покращенням CLS за всю історію метрики. Серйозно, це безкоштовне покращення — просто не ламайте його.
Щоб перевірити підтримку bfcache на вашому сайті:
- Відкрийте DevTools → вкладка Application → Back/Forward Cache
- Натисніть Test back/forward cache
- DevTools покаже, чи сторінку було успішно відновлено з bfcache, або перелічить причини блокування
Найчастіші блокатори bfcache та як їх прибрати:
// unload event блокує bfcache — не використовуйте його
window.addEventListener('unload', () => {
sendAnalytics();
});
// Замість unload використовуйте pagehide
window.addEventListener('pagehide', (event) => {
sendAnalytics();
// event.persisted === true означає,
// що сторінка може бути відновлена з bfcache
});
// Або використовуйте visibilitychange
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
sendAnalytics();
}
});
Також корисний NotRestoredReasons API для моніторингу в продакшні — він покаже, чому саме bfcache не спрацював у конкретних випадках:
// Моніторинг причин блокування bfcache
window.addEventListener('pageshow', (event) => {
if (!event.persisted) {
// Сторінка НЕ з bfcache — перевірте причини
const entry = performance
.getEntriesByType('navigation')[0];
if (entry?.notRestoredReasons) {
console.log(
'bfcache заблоковано:',
entry.notRestoredReasons
);
}
}
});
CSS contain для ізоляції зсувів
Властивість contain обмежує вплив зсувів макета рамками конкретного елемента. Зсув всередині контейнера не поширюється на решту сторінки:
/* Ізолюємо потенційно нестабільні секції */
.widget-container {
contain: layout style;
}
.sidebar-ads {
contain: layout size style;
}
/* Повна ізоляція для динамічного контенту */
.dynamic-section {
contain: strict;
/* еквівалент: contain: size layout style paint; */
}
Моніторинг CLS у продакшні
Порогові алерти
Google використовує дані CrUX (Chrome User Experience Report) за останні 28 днів для ранжування. Тому виставляйте алерти заздалегідь — на 80% від порогу:
- CLS > 0,08 → попередження (наближаєтесь до межі)
- CLS > 0,1 → критичний алерт (вже за межею «добре»)
Лабораторні vs польові дані
Lighthouse вимірює CLS тільки під час початкового завантаження. Але в реальності зсуви часто трапляються при скролінгі, клацанні та інших взаємодіях — і Lighthouse їх просто не бачить. Тому польові дані (CrUX, RUM) завжди пріоритетніші. Саме їх Google враховує для ранжування.
Проактивний аудит
Додайте перевірку CLS у ваш CI/CD pipeline — нехай зсуви ловляться ще до деплою:
# Lighthouse CI — автоматична перевірка CLS при кожному деплої
lhci autorun --collect.url=https://example.com \
--assert.assertions.cumulative-layout-shift=warn:0.08 \
--assert.assertions.cumulative-layout-shift=error:0.1
Чек-ліст CLS-оптимізації на 2026 рік
Ось стислий перелік дій у порядку пріоритету. Почніть з першого пункту — він закриє найбільшу частку проблем:
- Додайте width і height до всіх
<img>,<video>та<iframe>— це вирішує найчастішу проблему - Зарезервуйте місце для реклами через
min-heightтаaspect-ratio - Налаштуйте font metric overrides або використовуйте
font-display: optionalдля body-тексту - Попередньо завантажуйте шрифти через
<link rel="preload"> - Замініть layout-анімації на transform/opacity
- Увімкніть bfcache — видаліть
unloadevent listeners - Додайте contain-intrinsic-size до елементів із
content-visibility: auto - Моніторьте CLS через RUM і налаштуйте алерти на рівні 0,08
FAQ
Що таке CLS і який показник вважається хорошим?
CLS (Cumulative Layout Shift) — це метрика Core Web Vitals, що вимірює візуальну стабільність сторінки. Хорошим вважається показник 0,1 або менше на 75-му перцентилі. Значення від 0,1 до 0,25 потребує покращення, а понад 0,25 — вже погано. Google використовує CLS як сигнал ранжування, тому високий CLS може вплинути на позиції вашого сайту в пошуковій видачі.
Чому мій CLS у Lighthouse нормальний, а в Search Console — поганий?
Lighthouse вимірює CLS тільки під час початкового завантаження в контрольованому середовищі. Але в реальному житті зсуви часто трапляються пізніше — при скролінгі, клацанні, завантаженні реклами чи lazy-loaded зображень. Search Console використовує польові дані (CrUX) від реальних користувачів, які охоплюють весь сеанс. Тому завжди орієнтуйтесь саме на польові дані.
Як шрифти впливають на CLS і як це виправити?
Коли кастомний шрифт завантажується із затримкою, браузер показує текст fallback-шрифтом. При заміні на кастомний текст може перерозподілитися через різницю в метриках — і це спричиняє зсув. Найпростіше рішення — font-display: optional (нульовий CLS). Якщо потрібен кастомний шрифт завжди — використовуйте font-display: swap із font metric overrides (size-adjust, ascent-override) та preload.
Чи впливає lazy loading на CLS?
Так, lazy loading може погіршити CLS, якщо зображенням не задано width і height. Коли lazy-loaded зображення з'являється у viewport без зарезервованого місця, воно «штовхає» контент нижче. Завжди вказуйте розміри навіть для lazy-loaded зображень. І не використовуйте loading="lazy" для зображень у верхній частині сторінки (above the fold).
Що таке bfcache і як він допомагає з CLS?
Back/forward cache (bfcache) — оптимізація браузера, що зберігає повний знімок сторінки в пам'яті при навігації. Коли користувач натискає «Назад», сторінка миттєво відновлюється без жодних зсувів (CLS = 0). Близько 20% мобільних навігацій — це переходи назад/вперед, тому bfcache суттєво покращує загальний CLS. Щоб не зламати bfcache, уникайте unload event listeners та Cache-Control: no-store.