Нововведення Google, Core Web Vitals, поглинуло світ SEO та веб-розробки, і багато сайтів зайняті оптимізацією свого Page Experience, щоб підвищити фактор ранжування (зі статтею Як оптимізувати сайт для основних веб-показників Google можна ознайомитися за наступним посиланням). Метрика Cumulative Layout Shift спричиняє проблеми багатьом власникам сайтів, тому розгляньмо способи їх вирішення.
Cumulative Layout Shift (CLS) намагається виміряти в балах кожне зрушення контенту на сторінці, наприклад коли зображення, реклама або щось іще – з’являється пізніше, ніж основна частина сторінки. Він розраховує оцінку, засновану на тому, наскільки велика частина сторінки несподівано переміщується і як часто. Такі зміщення контенту дуже дратують, змушуючи вас втрачати місце в статті, яку ви почали читати, або, що ще гірше, змушуючи вас натиснути не на ту кнопку!
У цій статті я розповім про деякі front-end прийоми для зменшення CLS. Я не буду багато говорити про вимірювання CLS, оскільки про це багато написано в інших статтях. Я також не буду багато говорити про механіку розрахунку CLS: Google має хорошу документацію з цього питання, а Джесс Пек у своїй книзі “Майже повне керівництво з кумулятивного зсуву макета” (The Almost-Complete Guide to Cumulative Layout Shift) робить досить глибоке занурення в цю тему. Але спершу, додамо трохи теорії, необхідної для розуміння деяких прийомів.
Відмінності Cumulative Layout Shift
CLS, на мій погляд, є найцікавішим з основних веб-показників, почасти тому, що це те, що ми ніколи раніше не вимірювали і не оптимізували. Тому для його оптимізації часто потрібні нові методи і способи мислення. Це зовсім інший звір, ніж два інших основних веб-показника Core Web Vitals.
Якщо коротко розглянути два інших основних веб-показника, то Largest Contentful Paint (LCP) робить саме те, що випливає з його назви, і є скоріше логічним продовженням попередніх метрик завантаження, що вимірюють, наскільки швидко завантажується сторінка. Так, ми змінили спосіб визначення користувацького досвіду завантаження сторінки, щоб дивитися на швидкість завантаження найрелевантнішого контенту, але по суті це повторне використання старих методів забезпечення того, щоб контент завантажувався якомога швидше. Як оптимізувати LCP для більшості веб-сайтів, по ідеї не повинно викликати питань.
Затримка після першого введення (FID) вимірює будь-які затримки під час взаємодії користувача з сайтом, і також не повинна стати проблемою для більшості сайтів. Для поліпшення цього параметра зазвичай достатньо оптимізувати (або зменшити) JavaScript у кожного конкретного сайту. Це не означає, що вирішити проблеми з цими двома метриками легко, але це досить добре вивчені проблеми.
Одна з причин відмінності CLS полягає в тому, що він вимірюється протягом усього часу життя сторінки. Дві інші метрики Core Web Vitals припиняють вимірювання після виявлення основного компонента на сторінці після завантаження (для LCP) або до першої взаємодії (для FID). Це означає, що традиційні інструменти, такі як Lighthouse, часто не зовсім коректно відображають CLS, оскільки вони розраховують тільки CLS початкового завантаження. У реальності ж, користувач прокручуватиме сторінку вниз і може отримати більшу кількість контенту, що в підсумку може призвести до більшого зміщення.
По суті CLS є дещо штучною метрикою, яка розраховується на основі того, наскільки і як часто зсувається сторінка. Тоді як LCP і FID вимірюються в мілісекундах, CLS – це число без одиниць виміру, що отримується шляхом складного розрахунку. Необхідно прагнути, щоб CLS була 0.1 або менше. Усе, що вище 0.25, розглядається як “погано“.
Зрушення, спричинені взаємодією з користувачем, не враховуються. Це визначається як час у межах 500 мс після певного набору дій користувача, за винятком подій курсору миші та прокрутки. Передбачається, що користувач, який натискає на кнопку, може очікувати появи контенту, наприклад, розкриття згорнутого розділу.
CLS – це вимірювання несподіваних зрушень. Прокрутка не повинна викликати зсуву вмісту, якщо сторінка зверстана оптимально, і точно так само наведення курсору на зображення продукту, щоб отримати, наприклад, масштабовану картинку, не повинно викликати стрибків прилеглого контенту. Але, звісно, бувають винятки, і власникам таких сайтів необхідно продумати, як на це реагувати.
CLS також постійно вдосконалюється, вносячи зміни і виправляючи помилки. Нещодавно було анонсовано велике оновлення, яке має позитивно позначитися насамперед на сторінках з тривалим терміном існування, таких як односторінкові застосунки (SPA) і сторінки з нескінченною прокруткою, які, на думку багатьох, несправедливо “обділені” в CLS. Замість того, щоб накопичувати зрушення за весь час існування сторінки для розрахунку оцінки CLS, як це робили дотепер, оцінку розраховуватимуть на основі найбільшого набору зрушень у певному часовому інтервалі.
Це означає, що якщо у вас є три значення CLS 0.05, 0.06 і 0.04, то раніше це було б записано як 0.15 (тобто перевищення “хорошої” межі в 0.1), тоді як тепер це буде оцінено як 0.06. Це, як і раніше, узагальнено, у тому сенсі, що оцінка може складатися з окремих зсувів упродовж цього періоду часу (тобто якщо оцінка 0.06 CLS була спричинена трьома окремими зсувами на 0.02), але вона більше не сукупна впродовж усього часу життя сторінки.
Якщо ж ви усунете причини зсуву на 0.06, то ваш CLS буде представлений як наступний за величиною зсув (0.05). Таким чином, система, як і раніше, розглядає всі зрушення за час існування сторінки – просто вона вирішує представити як оцінку CLS тільки найбільший із них.
Після цього короткого введення в методологію CLS давайте перейдемо до рішень цієї проблеми! Усі ці методи здебільшого мають на увазі резервування певної області сторінки для завантаження додаткового контенту – чи то медіа, чи то контент із JavaScript.
Встановіть ширину і висоту для зображень і IFrame
Одна з найпростіших речей, які ви можете зробити для зменшення CLS – це встановити атрибути width
і height
для ваших зображень. Без них зображення викликатиме зміщення подальшого вмісту, щоб звільнити місце для нього після завантаження:

Для цього потрібно просто змінити розмітку зображення:
<img src="hero_image.jpg" alt="...">
на:
<img src="hero_image.jpg" alt="..."
width="400" height="400">
Розміри зображення можна дізнатися, відкривши DevTools і навівши курсор на елемент.

Я раджу використовувати Intrinsic Size (це фактичний розмір вихідного зображення), а браузер потім зменшить їх до оптимального розміру, залежно від використовуваного в’юпорта.
Якщо у вас є адаптивні зображення (докладну статтю про зображення в WordPress можна прочитати за цим посиланням) і ви використовуєте CSS для зміни розмірів зображення (наприклад, max-width
100% від розміру екрана), то ці атрибути можуть бути використані для розрахунку висоти – за умови, що ви встановили в стилях її значення в auto
:
img {
max-width: 100%;
height: auto;
}
Зараз це підтримують усі сучасні браузери, хоча до недавнього часу все було не так оптимістично. Це також працює для елементів picture
і зображень srcset
(встановлення ширини і висоти для fallback елемента img
), але поки що не для зображень з різним aspect-ratio – над цим працюють, а поки що вам все одно слід встановити ширину і висоту, оскільки будь-які значення будуть кращими, ніж 0
на 0
за замовчуванням!
Також цей метод працює із зображеннями, завантаженими в режимі ледачого завантаження (хоча Safari поки що не підтримує ледаче завантаження за замовчуванням).
Нова CSS властивість aspect-ratio
Наведену вище техніку розрахунку ширини і висоти для адаптивних зображень може бути поширено на інші елементи за допомогою нової CSS властивості aspect-ratio
, яке тепер підтримується браузерами на основі Chromium та Firefox, а також перебуває в Safari Technology Preview, тож, сподіваюся, воно скоро з’явиться і в стабільній версії.
Таким чином, ви можете використовувати його, наприклад, для вбудованого відео у співвідношенні 16:9:
video {
max-width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
<video controls width="1600" height="900" poster="...">
<source src="/media/video.webm"
type="video/webm">
<source src="/media/video.mp4"
type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
Цікаво, що без визначення властивості aspect-ratio
браузери ігноруватимуть висоту адаптивних відеоелементів і використовуватимуть співвідношення сторін за замовчуванням 2:1, тому вищевказаний спосіб необхідний, щоб уникнути зсуву макета.
У майбутньому можна буде навіть динамічно встановлювати співвідношення сторін на основі атрибутів елемента, використовуючи aspect-ratio: attr(width) / attr(height);
але, на жаль, це поки що не підтримується.
Або ж ви можете використовувати aspect-ratio
для елемента якогось користувацького елемента керування, який ви створюєте, щоб зробити його адаптивним:
#my-square-custom-control {
max-width: 100%;
height: auto;
width: 500px;
aspect-ratio: 1;
}
<div id="my-square-custom-control"></div>
У тих браузерах, які не підтримують aspect-ratio
, можна використовувати старий хак padding-bottom, але, з огляду на простоту нового aspect-ratio
і широку підтримку (особливо після переходу з Safari Technical Preview у звичайний Safari), важко виправдати використання цього методу.
Chrome – єдиний браузер, який передає дані CLS в Google, і він підтримує aspect-ratio
, що означає, що він вирішить ваші проблеми з CLS з точки зору Core Web Vitals. Мені не подобається ідея віддавати пріоритет метрикам, а не користувачам, але те, що в браузерах Chromium і Firefox це є, а в Safari, сподіваюся, скоро з’явиться, і що це прогресивне поліпшення, то я б сказав, що ми перебуваємо на тому етапі, коли можна залишити хак padding-bottom
позаду і писати чистіший код.
Вільно використовуйте min-height
Для тих елементів, яким не потрібна адаптивність, а потрібна фіксована висота, можна використовувати min-height
. Наприклад, це може бути заголовок фіксованої висоти, і ми можемо мати різні заголовки для різних брекпоїнтів, використовуючи, як зазвичай, медіа-запити:
header {
min-height: 50px;
}
@media (min-width: 600px) {
header {
min-height: 200px;
}
}
<header>
...
</header>
Звичайно, те ж саме стосується і до min-width
для горизонтально розташованих елементів, але зазвичай проблеми з CLS виникають через висоту.
Більш просунутий метод для контенту, що вставляється, і розширених селекторів CSS – націлювання на те, коли очікуваний контент ще не був вставлений. Наприклад, якщо у вас є такий вміст:
<div class="container">
<div class="main-content">...</div>
</div>
А додатковий div
вставляється за допомогою JavaScript:
<div class="container">
<div class="additional-content">.../div>
<div class="main-content">...</div>
</div>
Тоді ви можете використовувати наступний фрагмент коду, щоб зарезервувати місце для додаткового вмісту, коли div main-content
відображається спочатку.
.main-content:first-child {
margin-top: 20px;
}
Цей код фактично створить зміщення елемента main-content
, оскільки margin вважається частиною цього елемента, тому при видаленні margin він здаватиметься зміщеним (хоча насправді він не переміщується на екрані). Однак, принаймні, вміст під ним не буде зміщено, що має зменшити CLS.
Як альтернативу можна використовувати псевдоелемент ::before
для додавання відступу, щоб уникнути зсуву і для елемента main-content
:
.main-content:first-child::before {
content: '';
min-height: 20px;
display: block;
}
Але, чесно кажучи, найкращим рішенням буде розміщення div
в HTML і використання для нього min-height
.
Перевірка елементів fallback
Мені подобається використовувати прогресивні методи для розробки звичайних веб-сайтів без JavaScript. На жаль, нещодавно це послужило мені погану службу на одному підтримуваному сайті, коли версія без JavaScript відрізнялася від версії з JavaScript.
Проблема була пов’язана з кнопкою меню “Зміст” у заголовку. До ввімкнення JavaScript це просте посилання, стилізоване під кнопку, яке веде на сторінку змісту. Після ввімкнення JavaScript вона стає динамічним меню, що дає змогу переходити безпосередньо на потрібну сторінку.

Я використовував семантичні елементи і тому застосував якір (‹a href="#table-of-contents"›
) для зворотного посилання, але замінив його на ‹button›
для динамічного меню, керованого JavaScript. Ці елементи зовні виглядали однаково, але посилання виявилося на пару пікселів меншим, ніж кнопка!
Це було так незначно, а JavaScript зазвичай спрацьовував так швидко, що я не помітив цього. Однак Chrome помітив це під час розрахунку CLS і, оскільки посилання перебувало в заголовку, змістив усю сторінку на кілька пікселів униз. Це досить сильно вплинуло на оцінку CLS – достатньо, щоб усі наші сторінки потрапили в категорію “Потребує поліпшення”.
Це була помилка з мого боку, і виправлення полягало в тому, щоб просто привести два елементи у відповідність (це також можна було виправити, встановивши min-height
для заголовка, як говорилося вище), але мене це трохи збентежило. Я впевнений, що я не єдиний, хто припустився цієї помилки, тому будьте уважні до того, як сторінка відображається без JavaScript. Думаєте, що ваші користувачі не відключають JavaScript? Усі ваші користувачі не використовують JS, поки сторінка довантажує ваші скрипти.
Веб-шрифти викликають зсув у макеті
Веб-шрифти є ще однією поширеною причиною CLS, оскільки браузер спочатку розраховує необхідний простір для резервного шрифту, а потім перераховує його під час завантаження веб-шрифту. Зазвичай CLS невеликий, якщо використовується аналогічний за розміром резервний шрифт, тому часто вони не спричиняють досить серйозних проблем, щоб не пройти перевірку Core Web Vitals, але, тим не менш, можуть спостерігатися ривки вмісту сторінки, що дратують користувачів.

На жаль, навіть попереднє завантаження веб-шрифтів тут не допоможе, оскільки, хоч це і зменшує час використання резервних шрифтів (що добре для продуктивності завантаження – LCP), все одно потрібен час для їхнього одержання, і тому в більшості випадків резервні шрифти однаково використовуватимуться браузером, що не дає змоги уникнути CLS. Однак якщо ви знаєте, що веб-шрифт необхідний на наступній сторінці (наприклад, ви перебуваєте на сторінці входу в систему і знаєте, що на наступній сторінці використовується спеціальний шрифт), ви можете попередньо завантажити його.
Щоб повністю уникнути зрушень макета, спричинених шрифтами, можна, звісно, взагалі не використовувати веб-шрифти – зокрема використовувати замість них системні шрифти або використовувати font-display: optional
, щоб не використовувати їх, якщо вони не завантажені вчасно для початкового рендерингу. Але, якщо чесно, жоден із цих варіантів не є задовільним.
Інший варіант – задати відповідний розмір секцій (наприклад, за допомогою min-height
), так що, якщо навіть текст у них трохи зміститься, вміст під ними не буде зсунуто вниз. Наприклад, встановлення min-height
для елемента ‹h1›
може запобігти зміщенню всієї статті донизу під час завантаження вищих шрифтів – за умови, що різні шрифти не призводять до різної кількості рядків. Це зменшить вплив зсувів, однак у більшості випадків (наприклад, загальних абзаців) буде складно узагальнити мінімальну висоту.
Найбільше мене радує розв’язання цієї проблеми за допомогою нових дескрипторів шрифтів CSS, які дадуть вам змогу легше налаштовувати резервні шрифти в CSS:
@font-face {
font-family: 'Lato';
src: url('/static/fonts/Lato.woff2') format('woff2');
font-weight: 400;
}
@font-face {
font-family: "Lato-fallback";
size-adjust: 97.38%;
ascent-override: 99%;
src: local("Arial");
}
h1 {
font-family: Lato, Lato-fallback, sans-serif;
}
До цього для налаштування резервного шрифту потрібно було використовувати API завантаження шрифтів у JavaScript, що було складніше, але ця опція, яка дуже скоро з’явиться, нарешті надасть нам більш просте рішення, яке, найімовірніше, набуде поширення.
Початкові шаблони для рендерингових сторінок на стороні клієнта
Багато клієнтських візуалізаційних сторінок, або Single Page Apps, створюють початкову базову сторінку, використовуючи тільки HTML і CSS, а потім “пов’язують” її після завантаження і виконання JavaScript.
Ці початкові шаблони легко розсинхронізувати з версією JavaScript, оскільки нові компоненти та функції додаються до додатка в JavaScript, але не додаються до початкового HTML-шаблону, що відтворюється першим. Це призводить до виникнення CLS, коли ці компоненти впроваджуються в JavaScript.
Тому перегляньте всі свої початкові шаблони, щоб переконатися, що вони як і раніше є правильними. І якщо початковий шаблон складається з порожніх ‘div’, то використовуйте описані вище прийоми, щоб переконатися, що вони мають відповідний розмір, щоб спробувати уникнути зміщень макета.
Крім того, початковий div
, який вводиться в додаток, повинен мати min-height
, щоб він не відображався з висотою 0 ще до вставки початкового шаблону.
<div id="app" style="min-height:900px;"></div>
Якщо min-height
більше, ніж для більшості в’юпортів, це дасть змогу уникнути CLS, наприклад, для нижнього колонтитула сайту. CLS вимірюється тільки тоді, коли він перебуває в області перегляду і, таким чином, впливає на користувача. За замовчуванням порожній div
має висоту 0px, тому задайте йому min-height
, яка буде ближчою до фактичної висоти під час завантаження програми.
Забезпечення завершення взаємодії з користувачем протягом 500 мс
Взаємодія з користувачем, що викликає зміщення вмісту, виключається з оцінки CLS, і вона обмежена 500 мс після початку взаємодії. Тому якщо ви натиснете на кнопку, виконаєте складну обробку, яка займе понад 500 мс, а потім відобразите новий контент, ваш показник CLS погіршиться.
Ви можете перевірити, чи був виключений зсув, у Chrome DevTools, використовуючи вкладку Performance для запису сторінки, а потім знайшовши зсуви, як показано на наступному скріншоті. Відкрийте DevTools, перейдіть на дуже страшну (але після освоєння, дуже корисну!) вкладку Performance, потім натисніть на кнопку запису в лівому верхньому кутку (обведена на малюнку нижче) і взаємодійте зі своєю сторінкою, зупинивши запис після завершення.

На вкладці Performance, під рядком Experience, Chrome помістить червонувато-рожеву рамку для кожного зсуву, під час натискання на яку ви отримаєте розгорнутішу інформацію на вкладці Summary нижче.
Тут видно, що ми отримали абсолютно неприйнятний результат 0.3359 – набагато більший за допустимий поріг 0.1, але в сумарному показнику це не враховується, оскільки для параметра Had recent input встановлено значення Uses.
Забезпечення взаємодії тільки зміщує вміст у межах 500 мс, які оцінює First Input Delay (FID), але є випадки, коли користувач може бачити, що введення вплинуло (наприклад, відображається індикатор завантаження), тому FID хороший, але вміст не може бути додано на сторінку до закінчення 500 мс, тому для CLS – це погано.
В ідеалі вся взаємодія не повинна перевищувати 500 мс, але ви можете зробити деякі кроки, щоб зарезервувати необхідний простір за допомогою описаних вище методів, поки триває обробка, тож якщо вона займе більше, ніж магічні 500 мс, то ви вже впоралися зі зсувом і CLS не постраждає. Це особливо корисно під час отримання контенту з мережі, який може бути динамічним і не контролюватися вами.
Також слід звернути увагу на анімацію, яка завантажується більше 500 мс і може вплинути на CLS. Незважаючи на гадану суворість і декларативність, мета CLS – не обмежити “веселощі”, а встановити розумні очікування щодо користувацького досвіду, і не зовсім правильно очікувати, що анімація займатиме 500 мс або менше. Але якщо ви з цим не згодні, або у вас є унікальний досвід використання, то команда Chrome відкрита для зворотного зв’язку з цього питання.
Синхронний JavaScript
Остання техніка, яку я збираюся обговорити, трохи спірна, оскільки вона суперечить відомим порадам щодо продуктивності веб-сайтів, але в певних ситуаціях вона може виявитися єдиним рішенням. У принципі, якщо у вас є контент, який, як передбачається, може викликати зсув, то одне з рішень, що запобігають зсувам, – це не відображати його, поки він не стабілізується!
Наведений нижче HTML спочатку приховає div
, потім завантажить скрипт, що блокує рендеринг, щоб заповнити контейнер, а потім приховає його. Оскільки JavaScript блокує рендеринг, нічого нижче цього не буде рендеринговано (включно з другим блоком зі стилями), і тому зрушень не відбудеться.
<style>
.cls-inducing-div {
display: none;
}
</style>
<div class="cls-inducing-div"></div>
<script>
...
</script>
<style>
.cls-inducing-div {
display: block;
}
</style>
Під час використання цієї техніки важливо вставляти стилі в HTML розмітку, щоб вони застосовувалися по порядку. Альтернативою є приховування вмісту за допомогою JavaScript, але мені подобається вищеописана техніка тим, що вона приховує вміст, навіть якщо JavaScript не працює або його вимкнено в браузері.
Цю техніку можна застосовувати навіть із зовнішнім JavaScript, але це може спричинити більшу затримку, ніж під час використання вбудованих скриптів, оскільки зовнішній JavaScript необхідно ще завантажити. Цю затримку можна мінімізувати, попередньо завантаживши скрипти, щоб не було затримок, коли парсер дійде до цієї ділянки коду:
<head>
...
<link rel="preload" href="cls-inducing-javascript.js" as="script">
...
</head>
<body>
...
<style>
.cls-inducing-div {
display: none;
}
</style>
<div class="cls-inducing-div"></div>
<script src="cls-inducing-javascript.js"></script>
<style>
.cls-inducing-div {
display: block;
}
</style>
...
</body>
Як уже йшлося вище, я впевнений, що ці техніки змусять деяких спеців з оптимізації скривитись, оскільки порада використовувати async, defer
або новіший type="module"
(які відкладено за замовчуванням) спеціально для того, щоб уникнути блокування візуалізації, тоді як ми тут намагаємося домогтися зворотного! Однак, якщо контент не може бути заздалегідь зумовлений, і він спричинятиме різкі зміщення, то немає сенсу рендерити його завчасно.
Я використовував цю техніку для банера cookie, який завантажувався у верхній частині сторінки і зміщував вміст вниз:

Це вимагало зчитування cookie, щоб визначити, показувати банер чи ні, і, хоча це можна було зробити на боці сервера, в даному випадку це був статичний сайт без можливості динамічно змінювати HTML, що повертається.
Банери cookie можна реалізувати різними способами, щоб уникнути CLS. Наприклад, розташувати їх у нижній частині сторінки або накласти поверх контенту, а не зміщувати контент вниз. Ми вважали за краще залишити контент у верхній частині сторінки, тому довелося використовувати цей прийом, щоб уникнути зміщення.
Цю техніку використовується і на іншій сторінці, де JavaScript переміщує вміст у “головну” і “побічну” колонки (з низки причин цього неможливо було домогтися на стороні сервера). Знову зміщення контенту, поки JavaScript не переставить його, і тільки потім показавши його, вдалося уникнути проблем з CLS, які призводили до зниження оцінки Core Web Vitals цих сторінок.
Використання цієї техніки може вплинути на інші показники (зокрема, LCP і First Contentful Paint), оскільки ви блокуєте рендеринг сторінки, а також потенційно блокуєте передзавантаження браузера, але це ще один інструмент, який варто розглянути в тих випадках, коли інших варіантів немає.
Висновок
Cumulative Layout Shift (CLS) викликається зміною розмірів контенту або впровадженням нового контенту на сторінку за допомогою пізно запущеного JavaScript. У цій статті ми розглянули різні поради та прийоми, що дають змогу уникнути цих проблем. Я радий, що Core Web Vitals звернув увагу на цю дратівливу проблему – занадто довго ми, веб-розробники (і я, безумовно, відношу себе до них), ігнорували її.
Оптимізація моїх власних веб-сайтів призвела до зростання поведінкових факторів. Я закликаю вас також звернути увагу на проблеми CLS, і сподіваюся, що деякі з цих порад будуть вам корисні. Хто знає, можливо, вам навіть вдасться домогтися невловимого показника CLS 0 для всіх ваших сторінок!
Джерело: https://www.smashingmagazine.com/