Оптимізація CLS у 2026 році: як зробити сайт візуально стабільним

Як діагностувати й виправити зсуви макета (CLS) у 2026 році. Практичні техніки: font metric overrides, bfcache, CSS contain та правильна робота зі шрифтами, зображеннями й рекламою — з прикладами коду.

Чому 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, щоб отримати оцінку «добре».

Формула обчислення

Кожен окремий зсув макета оцінюється за двома компонентами:

  1. Impact Fraction (частка впливу) — яку частину viewport займає зона, уражена зсувом (об'єднання старої та нової позицій елемента)
  2. 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).

Як цим користуватися:

  1. Відкрийте DevTools (F12) → вкладка Performance
  2. Увімкніть опцію Web Vitals
  3. Запишіть трейс при завантаженні сторінки
  4. У панелі Insights знайдіть секцію Layout Shift Culprits
  5. Кожен кластер покаже елементи, що зсунулися, причину та амплітуду зсуву

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 на вашому сайті:

  1. Відкрийте DevTools → вкладка ApplicationBack/Forward Cache
  2. Натисніть Test back/forward cache
  3. 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 рік

Ось стислий перелік дій у порядку пріоритету. Почніть з першого пункту — він закриє найбільшу частку проблем:

  1. Додайте width і height до всіх <img>, <video> та <iframe> — це вирішує найчастішу проблему
  2. Зарезервуйте місце для реклами через min-height та aspect-ratio
  3. Налаштуйте font metric overrides або використовуйте font-display: optional для body-тексту
  4. Попередньо завантажуйте шрифти через <link rel="preload">
  5. Замініть layout-анімації на transform/opacity
  6. Увімкніть bfcache — видаліть unload event listeners
  7. Додайте contain-intrinsic-size до елементів із content-visibility: auto
  8. Моніторьте 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.

Про Автора Editorial Team

Our team of expert writers and editors.