Cumulative Layout Shift (CLS)

Накопительный сдвиг макета (Cumulative Layout Shift, далее — CLS) — важный, ориентированный на пользователя, показатель для измерения его визуальной стабильности, он помогает количественно оценить, как часто пользователи сталкиваются с неожиданными сдвигами макета. Низкий уровень CLS говорит о том, что страница выглядит восхитительно.

Вы когда-нибудь читали онлайн-статью, когда на странице что-то внезапно меняется? Без предупреждения текст перемещается и вы теряете это место. Или, что ещё хуже: собираетесь нажать на ссылку или кнопку, но за мгновение до того, как ваш палец или курсор мыши приземлится — БУМ — ссылка перемещается, и вы в конечном итоге нажимаете что-то другое!

В большинстве случаев подобные вещи вызывают раздражение, но иногда они могут нанести реальный ущерб.

Видео, демонстрирующее, негативные последствия нестабильности макета.

Неожиданное перемещение содержимого страницы обычно происходит из-за асинхронной загрузки ресурсов или динамического добавления DOM-элементов на страницу над уже существующим содержимым. Причиной может быть изображение или видео с неизвестными размерами, шрифт, который отображается крупнее или меньше, чем его запасной вариант, сторонние объявления или виджеты, которые динамически изменяют размер.

Ещё сильнее эта проблема усугубляется тем, что функционирование сайта в процессе разработки может сильно отличаться от того, как его воспринимают пользователи. Персонализированный или сторонний контент часто ведёт себя иначе, чем при разработке, например: тестовые изображения обычно уже находятся в кэше браузера разработчика, а вызовы API, которые выполняются локально, получают ответ сервера слишком быстро и возможные задержки не заметны.

Показатель накопительного сдвига макета (CLS — Cumulative Layout Shift) помогает решить эту проблему, измеряя, как часто это происходит у реальных пользователей.

Что такое CLS?

Cumulative Layout Shift измеряет общую сумму всех индивидуальных оценок сдвига макета для каждого неожиданного сдвига макета, который происходит в течение всего времени жизни страницы.

Сдвиг макета происходит каждый раз, когда видимый элемент изменяет свое положение от одного кадра отрисовки к другому (см. ниже подробности о том, как рассчитываются индивидуальные оценки сдвига макета).

Cumulative Layout Shift values

Что для CLS — хороший результат?

Чтобы обеспечить комфорт для пользователей, сайты должны стремиться к тому, чтобы показатель CLS не превышал 0.1. Чтобы убедиться в достижении этой цели для большинства пользователей, хорошим порогом для измерения будет 75-й перцентиль загрузки страниц, сегментированный по мобильным и настольным устройствам.

Подробности о сдвигах макета

Сдвиги макета определяются Layout Instability API (API нестабильности макета), который возвращает записи layout-shift о сдвиге макета каждый раз, когда элемент, видимый в области просмотра, меняет своё положение (например, его верхнюю и левую координаты для writing mode по умолчанию) между двумя кадрами. Такие элементы считаются нестабильными.

Обратите внимание, что сдвиги макета происходят только тогда, когда существующие элементы меняют своё положение. Если новый элемент добавляется в DOM-модель или существующий элемент меняет размер — это не будет считаться сдвигом макета до тех пор, пока изменения не заставляет другие видимые элементы менять своё положение.

Оценка сдвига макета

Чтобы вычислить оценку сдвига макета, браузер смотрит на размеры области просмотра (viewport) и перемещение нестабильных элементов в ней между двумя визуализированными кадрами. Оценка сдвига макета будет продуктом двух показателей этого движения: доля воздействия и доля расстояние (оба определены ниже).

layout shift score = доля воздействия * доля расстояния

Доля воздействия

Доля воздействия (impact fraction) измеряет, как нестабильные элементы влияют на область просмотра между двумя кадрами.

Объединение видимых областей всех нестабильных элементов для предыдущего кадра и текущего кадра, в виде доли от общей площади области просмотра, будет долей воздействия для текущего кадра.

доля воздействия

На изображении выше есть элемент, который занимает половину области просмотра в одном кадре. Затем в следующем кадре элемент сдвигается вниз на 25% от высоты области просмотра. Красный пунктирный прямоугольник указывает на объединение видимой области элемента в обоих кадрах, которая в данном случае составляет 75% от всего окна просмотра, поэтому его доля воздействия составляет 0.75.

Доля расстояния

Другая часть выражения оценки сдвига макета измеряет расстояние, на которое нестабильные элементы переместились относительно области просмотра. Доля расстояния — это наибольшее расстояние, на которое любой нестабильный элемент переместился в кадре (по горизонтали или вертикали), деленное на наибольший размер окна просмотра (ширина или высота, в зависимости от того, что больше).

доля расстояния

В приведенном выше примере наибольшим размером области просмотра является высота, а нестабильный элемент переместился на 25% от высоты области просмотра, доля расстояния составляет 0.25.

Первоначально оценка сдвига макета рассчитывалась только на основе доли воздействия. Доля расстояния была введена, чтобы избежать чрезмерного результата, когда большие элементы смещаются на небольшую величину.

В следующем примере показано, как добавление содержимого к существующему элементу влияет на оценку сдвига макета:

Сдвиг макета при добавлении контента

Кнопка «Click Me!» добавляется в нижнюю часть серого поля с черным текстом, который толкает зеленое поле с белым текстом вниз (и частично за пределы области просмотра).

В этом примере серый прямоугольник меняет размер, но его начальное положение не меняется, поэтому он не будет нестабильным элементом.

Кнопки «Click Me!» ранее не было в DOM, поэтому ее исходное положение также не изменится.

Начальное положение зеленого прямоугольника изменилось, но поскольку он частично переместился за пределы области просмотра, невидимая область не учитывается при вычислении доли воздействия. Объединение видимых областей для зеленого прямоугольника в обоих кадрах (показано красным пунктирным прямоугольником) совпадает с площадью зеленого прямоугольника в первом кадре — 50% области просмотра. Степень воздействия — 0.5.

Доля расстояния показана фиолетовой стрелкой. Зеленая рамка сместилась вниз примерно на 14% от области просмотра, поэтому доля расстояния составляет 0.14.

Оценка сдвига макета составляет 0.5 x 0.14 = 0.07.

Этот пример иллюстрирует несколько нестабильных элементов:

несколько нестабильных элементов

В первом кадре на картинке выше представлены четыре результата запроса к API для животных, отсортированных в алфавитном порядке. Во втором кадре в отсортированный список добавляется еще несколько результатов.

Первый элемент в списке («Cat») не меняет своё начальное положение между кадрами, поэтому он стабилен. Точно так же новые элементы, добавленные в список, ранее не были в DOM, поэтому их начальное положение также не меняется. Остальные элементы «Dog», «Horse» и «Zebra» меняют свои исходные позиции, что делает их нестабильными элементами.

Опять же, красные пунктирные прямоугольники представляют собой объединение этих трех нестабильных элементов до и после областей, которые в этом случае составляют около 38% площади области просмотра (доля воздействия 0.38).

Стрелки показывают расстояния, на которые нестабильные элементы переместились из своих исходных положений. Элемент «Zebra», обозначенный синей стрелкой, переместился дальше всего, примерно на 30% высоты области просмотра. Таким образом, доля расстояния в этом примере равна 0.3.

Оценка сдвига макета составляет 0.38 x 0.3 = 0.1172.

Ожидаемые и неожиданные сдвиги макета

Не все сдвиги в макете — это плохо. Фактически, многие динамические веб-приложения часто меняют начальное положение элементов на странице.

Изменения макета по инициативе пользователя

Сдвиг макета плох только в случае, когда пользователь этого не ожидает. С другой стороны, сдвиги макета, которые происходят в ответ на действия пользователя (клик по ссылке, нажатие кнопки, ввод в поле поиска и т.д.), как правило, допустимы, если сдвиг происходит настолько близко к взаимодействию, что взаимосвязь будет понятна для пользователя.

Например, если взаимодействие с пользователем запускает сетевой запрос, выполнение которого может занять некоторое время, лучше сразу освободить место и показать индикатор загрузки, чтобы избежать неприятного сдвига макета после завершения запроса. Если пользователь не понимает, что что-то загружается, или не знает, когда ресурс будет готов, он может попытаться кликнуть что-нибудь ещё во время ожидания.

Для сдвигов макета, которые происходят в течение 500 миллисекунд после ввода пользователя, будет установлен флаг hadRecentInput, поэтому их можно исключить из вычислений.

Внимание! Флаг hadRecentInput будет возвращать значение только для дискретных событий: касание, клик или нажатие клавиши. Непрерывные взаимодействия: прокрутка, перетаскивание или жесты сжатия и масштабирования — не считаются «недавним вводом». Дополнительные сведения см. в спецификации Layout Instability.

Анимации и переходы

Если всё сделано правильно, анимация и переходы будут отличным способом обновить контент на странице, не вызывая негативные реакции у пользователя. Контент, который резко и неожиданно перемещается на странице, почти всегда создает неудобства для пользователей. Но контент, который постепенно и естественно перемещается из одной позиции в другую, часто может помочь пользователю лучше понять: что происходит и что делать между изменениями состояния.

CSS-свойство transform позволяет анимировать элементы, не вызывая сдвигов макета:

  • Вместо изменения свойств высоты (height) и ширины (width) можно использовать transform: scale().
  • Чтобы перемещать элементы, следует избегать изменения свойств top, right, bottom или left и использовать вместо них transform: translate().

Как измерить CLS

CLS можно измерять в лабораторных условиях с использованием инструментов для имитации загрузки страницы в согласованной контролируемой среде или в полевых на реальных пользователях, которые по-честному загружают страницу и взаимодействуют с ней.

Тестирование производительности в лабораторных условиях имеет важное значение при разработке новых функций. До того, как они будут выпущены в производство, не получится измерить их характеристики на реальных пользователях. Поэтому тестирование этих функций при разработке, перед выпуском в прод — лучший способ обнаружить и предотвратить снижение производительности.

С другой стороны, хотя лабораторное тестирование является разумным показателем производительности, оно не обязательно отражает то, как разные пользователи будут воспринимать сайт вживую. Производительность сайта может сильно различаться в зависимости от возможностей устройства пользователя и условий работы его сети, а также варьироваться в зависимости от того, взаимодействует ли (или как) пользователь со страницей.

Более того, загрузка страниц не может быть детерминированной. Например, сайты, загружающие персонализированный контент или рекламу, могут иметь совершенно разные характеристики производительности от пользователя к пользователю. Лабораторный тест не выявит этих различий.

Единственный способ по-настоящему узнать, как сайт работает для пользователей — это измерять производительность в реальных условиях, когда живые пользователи его загружают и взаимодействуют с ним. Этот тип измерения обычно называется мониторингом реального пользователя (Real User Monitoring).

Инструменты

В полевых условиях

В лабораторных условиях

Измерение CLS с помощью JavaScript

Чтобы измерить CLS с помощью JavaScript, можно использовать Layout Instability API (API нестабильности макета). В примере ниже показано, как создать PerformanceObserver, который прослушивает неожиданные сдвиги макета (layout-shift), накапливает их и записывает в консоль:

let cls = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      cls += entry.value;
      console.log('Current CLS value:', cls, entry);
    }
  }
}).observe({type: 'layout-shift', buffered: true});

Предупреждение: этот код показывает, как регистрировать и накапливать записи о неожиданных сдвигах макета. Измерить CLS в JavaScript несколько сложнее. Подробности см. ниже.

В приведенном выше примере все записи layout-shift, для которых флаг hadRecentInput установлен в значение false, накапливаются для определения текущего значения CLS. В большинстве случаев текущее значение CLS на момент выгрузки страницы является окончательным значением CLS для этой страницы, но есть несколько важных исключений.

Далее будут перечислены различия между тем, что сообщает API, и тем, как рассчитывается метрика.

Различия между метрикой и API

  • Страница, которая находится в фоновом состоянии в течение всего своего жизненного цикла, не должна сообщать какое-либо значение CLS.
  • Для страницы, которая восстанавливается из кэша при навигации назад/вперед, значение CLS должно быть сброшено до нуля, поскольку пользователи воспринимают это как отдельное посещение страницы.
  • API не сообщает о записях layout-shift для сдвигов, которые происходят в окнах iframe, но для правильного измерения CLS вы должны их учитывать. Подкадры могут использовать API, чтобы сообщать о своих записях layout-shift в родительский кадр для агрегирования.

В дополнение к этим исключениям, CLS имеет дополнительную сложность из-за того, что он ведет измерения весь срок жизни страницы:

  • Пользователи могут держать вкладку открытой в течение очень долгого времени — дней, недель, месяцев. Фактически, пользователь может никогда не закрыть вкладку.
  • В мобильных операционных системах браузеры обычно не запускают обратные вызовы выгрузки страницы для фоновых вкладок, что затрудняет сообщение «финального» значения.

Чтобы справиться с такими случаями, CLS следует сообщать каждый раз, когда страница находится в фоновом режиме, в дополнение к любому времени, когда она выгружается (события visibilitychange достаточно на оба этих сценария). А аналитическим системам, получающим эти данные, необходимо будет вычислить окончательное значение CLS на бэкэнде.

Вместо того, чтобы запоминать и самостоятельно разбираться со всеми этими случаями, разработчики могут использовать JavaScript-библиотеку web-vitals для измерения CLS, которая учитывает всё, что упомянуто выше:

import {getCLS} from 'web-vitals';

// Измерение и запись CLS для всех ситуаций
// когда об этом требуется сообщать.
getCLS(console.log);

Можно посмотреть исходный код getCLS() для изучения примера того, как измерить CLS в JavaScript. В некоторых случаях (cross origin iframe) невозможно измерить CLS в JavaScript. Подробнее об этом в разделе ограничений библиотеки web-vitals.

Как улучшить CLS

Для большинства веб-сайтов можно избежать всех неожиданных сдвигов в макете, придерживаясь нескольких руководящих принципов:

  • Всегда следует включать атрибуты размеров для изображений и видеоэлементов или иным образом резервируйте необходимое пространство с помощью, например, CSS-свойства aspect-ratio. Такой подход предполагает, что браузер сможет заранее выделить правильный объем места в документе и использовать его во время загрузки изображения. Обратите внимание, что вы также можете использовать политику функции unsized-media, чтобы принудительно использовать это поведение в браузерах, поддерживающих политики функций.
  • Никогда не следует вставлять контент перед (выше) существующим, кроме случаев, когда это нужно в ответ на взаимодействие с пользователем. Это гарантирует, что любые изменения макета будут ожидаемыми.
  • Следует отдавать предпочтение CSS-transform анимации переходов вместо изменения свойств, запускающих изменение макета. Надо анимировать переходы таким образом, чтобы обеспечить контекст и непрерывность от состояния к состоянию.