Клавиша / esc

CLS (Cumulative Layout Shift)

Что такое метрика Cumulative Layout Shift в Core Web Vitals

Время чтения: 8 мин

Что такое 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-ratio. Тогда браузер заранее зарезервирует место с правильным соотношением сторон.

        
          
          <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 компонент <Image> требует 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-family всегда указывайте системный запасной шрифт ("Google Sans", sans-serif, а не просто "Google Sans"), иначе до загрузки шрифта текст рисуется дефолтным браузерным шрифтом с ещё большим расхождением метрик. Свойства size-adjust, ascent-override, descent-override и line-gap-override в @font-face позволяют подогнать запасной шрифт под кастомный, чтобы при подмене ничего не смещалось. Критичные шрифты предзагружайте через <link rel="preload">. Также уменьшайте размер файлов шрифтов: формат WOFF2 (сжатие Brotli, на ~30% легче WOFF), вариативные шрифты вместо отдельного файла на каждую толщину и наклон, а также сабсеттинг: нарежьте шрифт на файлы по диапазонам символов и подключайте нужный через unicode-range — тогда для кириллического текста браузер скачает только кириллицу, а не весь шрифт со всеми языками.

В Next.js next/font раздаёт шрифты со своего домена, а не с внешнего CDN, и подбирает метрики запасного шрифта через size-adjust, что помогает избежать сдвига при подмене.

        
          
          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-Control: no-store (Chrome постепенно снимает ограничения). Проверить страницу можно в DevTools → Application → Back/forward cache → Test: Chrome покажет список проблем, мешающих работе bfcache, и подскажет, какие из них можно исправить.

Подсказки

Скопировано

🛠 В DevTools на вкладке Performance есть дорожка Layout Shifts, она подсветит, какие именно элементы сдвигались и сколько баллов добавили. Очень помогает улучшить CLS.