Что такое CLS
СкопированоCLS (Cumulative Layout Shift) — одна из трёх основных метрик Core Web Vitals, набора показателей Google для оценки удобства сайта. CLS измеряет визуальную стабильность страницы: насколько сильно элементы страницы неожиданно перемещаются во время загрузки и после неё. Каждый раз, когда элемент, который уже отображается на странице, внезапно перемещается не из-за действия пользователя, браузер фиксирует сдвиг макета.
Отдельные сдвиги группируются в сессионные окна: если между сдвигами меньше секунды, они попадают в одно окно, а само окно не может длиться дольше пяти секунд. Итоговое значение CLS — это самое «дорогое» из таких окон за всё время жизни страницы, а не сумма вообще всех сдвигов, что случились за сессию.
Значение одного сдвига рассчитывается так: доля площади экрана, которую затронул сдвиг, × доля экрана, на которую переместился контент. Чем больше площадь и чем дальше перемещение, тем хуже. Например, знакомая ситуация: целитесь в кнопку «Купить» на сайте, в этот момент сверху догружается баннер, контент уезжает вниз, и вы случайно нажимаете «Отмена».
Запоминать формулу CLS необязательно. Хорошим считается CLS 0,1 и меньше, плохим — больше 0,25; полная таблица порогов для всех метрик — в статье «Что такое Core Web Vitals».
Из-за чего макет сдвигается
СкопированоЧаще всего причиной сдвигов становятся изображения без указанных размеров. Также часто причиной становятся поздно загружаемые шрифты и динамический контент: реклама, баннеры согласия с cookies, всплывающие плашки.
- картинки и видео без размеров — браузер не знает, сколько места нужно зарезервировать, пока файл не загрузится, поэтому сначала размещает остальные элементы без этого пространства, а после загрузки изображения сдвигает их;
- реклама, виджеты, встроенный контент вроде iframe с неизвестной заранее высотой, особенно если подгружаются асинхронно;
- динамический контент, вставленный над уже видимым: баннер согласия на обработку cookies над текстом, догрузившийся блок рекомендаций, сообщение об ошибке в момент рендеринга;
- веб-шрифты — пока кастомный шрифт загружается, браузер может показать текст запасным системным шрифтом. Когда загружается кастомный шрифт с другими метриками (шириной символов, высотой строки и другими параметрами), браузер пересчитывает размеры текста, и это вызывает сдвиг макета. Обычно это проявляется в двух сценариях: FOIT (Flash of Invisible Text) — когда текст временно скрыт до загрузки шрифта, и FOUT (Flash of Unstyled Text) — когда сначала показывается текст запасным шрифтом, а затем он заменяется на кастомный;
- анимации «не тех» свойств, когда двигают
top,left,margin,widthвместоtransform— такая анимация заставляет браузер пересчитывать макет, из-за чего могут смещаться соседние элементы.
Как улучшить CLS
СкопированоСначала убедитесь, что для изображений, видео, рекламы и других динамически появляющихся блоков заранее зарезервировано место. Затем проверьте шрифты, динамические вставки и анимации свойств, которые меняют раскладку страницы.
Задавайте размеры изображений и видео: атрибуты width и height либо CSS-свойство aspect. Тогда браузер заранее зарезервирует место с правильным соотношением сторон.
<img src="cat.jpg" alt="Кот" width="800" height="600" /><style> img { /* при адаптивной вёрстке размеры из атрибутов сохраняют пропорцию */ height: auto; width: 100%; }</style>
<img src="cat.jpg" alt="Кот" width="800" height="600" />
<style>
img {
/* при адаптивной вёрстке размеры из атрибутов сохраняют пропорцию */
height: auto;
width: 100%;
}
</style>
В Next.js компонент < требует width и height либо fill, поэтому место под изображение резервируется до его загрузки. При статическом импорте размеры подставляются автоматически.
Для srcset используйте изображения с одинаковым соотношением сторон. Для изображений через <img> или <picture> задавайте width и height. В современных браузерах можно задавать размеры и для каждого <source>.
Дальше несколько частых источников сдвигов и что с ними делать:
- резервируйте место под рекламу и эмбеды вроде iframe заранее: задайте контейнеру
minили- height aspectпод самый вероятный размер и учитывайте разные размеры на брейкпоинтах через медиазапросы;- ratio - баннеры cookie-уведомлений — частый и недооценённый источник CLS: на мобильных экранах такой баннер занимает заметную долю видимой области экрана, а его текст может стать самым крупным элементом на экране, который замеряет метрика LCP (Largest Contentful Paint, загрузка основного контента). Заранее устанавливайте соединение с доменом баннера через
preconnect/dns, если он внешний, и резервируйте место под него так же, как под любой другой поздно подгружаемый блок;- prefetch - не вставляйте контент над уже видимым без действия пользователя. Если этого избежать нельзя, размещайте его ниже текущей области просмотра или показывайте оверлеем поверх, а не сдвигая контент. Правило: чем ближе вставка к верхней части экрана, тем сильнее сдвиг. Тот же по размеру блок, добавленный внизу видимой области, может дать меньше баллов CLS;
- для бесконечной ленты и кнопки «Показать ещё» подгружайте контент по нажатию на кнопку или подготавливайте новый контент за пределами экрана. Есть три рабочих паттерна: менять контент внутри контейнера фиксированного размера (карусель, слайдер: старое исчезает, новое встаёт на то же место); догружать заранее, до того как пользователь долистает до конца блока или нажмёт на элемент (сдвиги, произошедшие в течение первых 500 мс после взаимодействия пользователя, не учитываются в CLS); загружать по явному действию — кнопка «Показать ещё» вместо автоподгрузки при скролле, когда пользователь явно ожидает изменение страницы.
Отдельная история — шрифты. В font всегда указывайте системный запасной шрифт (", а не просто "), иначе до загрузки шрифта текст рисуется дефолтным браузерным шрифтом с ещё большим расхождением метрик. Свойства size, ascent, descent и line в @font позволяют подогнать запасной шрифт под кастомный, чтобы при подмене ничего не смещалось. Критичные шрифты предзагружайте через <link rel. Также уменьшайте размер файлов шрифтов: формат WOFF2 (сжатие Brotli, на ~30% легче WOFF), вариативные шрифты вместо отдельного файла на каждую толщину и наклон, а также сабсеттинг: нарежьте шрифт на файлы по диапазонам символов и подключайте нужный через unicode — тогда для кириллического текста браузер скачает только кириллицу, а не весь шрифт со всеми языками.
В Next.js next раздаёт шрифты со своего домена, а не с внешнего CDN, и подбирает метрики запасного шрифта через size, что помогает избежать сдвига при подмене.
import { Geist } from "next/font/google";const geist = Geist({ subsets: ["latin"] });export default function RootLayout({ children }) { return ( <html className={geist.className}> <body>{children}</body> </html> );}
import { Geist } from "next/font/google";
const geist = Geist({ subsets: ["latin"] });
export default function RootLayout({ children }) {
return (
<html className={geist.className}>
<body>{children}</body>
</html>
);
}
Анимируйте через transform (translate, scale, rotate), а не через top/left/margin/width: браузер применяет transform на финальном этапе отрисовки, когда макет уже посчитан, и это не вызывает перерасчёт макета соседних элементов.
Ещё один способ снизить CLS — включить bfcache (back/forward cache). Это кеш, из которого браузер мгновенно восстанавливает целую страницу при навигации «назад»/«вперёд», без сдвигов. Такие переходы не редкость: по данным web.dev, примерно 10% навигаций на десктопе и 20% на мобильных — это переходы «назад»/«вперёд». При восстановлении страницы из bfcache значения LCP, FCP и CLS обычно становятся близкими к нулю.
Чаще всего bfcache ломают обработчики unload (используйте pagehide) и заголовок Cache (Chrome постепенно снимает ограничения). Проверить страницу можно в DevTools → Application → Back/forward cache → Test: Chrome покажет список проблем, мешающих работе bfcache, и подскажет, какие из них можно исправить.
Подсказки
Скопировано🛠 В DevTools на вкладке Performance есть дорожка Layout Shifts, она подсветит, какие именно элементы сдвигались и сколько баллов добавили. Очень помогает улучшить CLS.