Накопительный сдвиг макета (Cumulative Layout Shift, далее – CLS) – важный, ориентированный на пользователя, показатель для измерения его визуальной стабильности, он помогает количественно оценить, как часто пользователи сталкиваются с неожиданными сдвигами макета. Низкий уровень CLS говорит о том, что страница выглядит восхитительно.
Вы когда-нибудь читали онлайн-статью, когда на странице что-то внезапно меняется? Без предупреждения текст перемещается и вы теряете это место. Или, что ещё хуже: собираетесь нажать на ссылку или кнопку, но за мгновение до того, как ваш палец или курсор мыши приземлится – БУМ – ссылка перемещается, и вы в конечном итоге нажимаете что-то другое!
В большинстве случаев подобные вещи вызывают раздражение, но иногда они могут нанести реальный ущерб.
Неожиданное перемещение содержимого страницы обычно происходит из-за асинхронной загрузки ресурсов или динамического добавления DOM-элементов на страницу над уже существующим содержимым. Причиной может быть изображение или видео с неизвестными размерами, шрифт, который отображается крупнее или меньше, чем его запасной вариант, сторонние объявления или виджеты, которые динамически изменяют размер.
Ещё сильнее эта проблема усугубляется тем, что функционирование сайта в процессе разработки может сильно отличаться от того, как его воспринимают пользователи. Персонализированный или сторонний контент часто ведёт себя иначе, чем при разработке, например: тестовые изображения обычно уже находятся в кэше браузера разработчика, а вызовы API, которые выполняются локально, получают ответ сервера слишком быстро и возможные задержки не заметны.
Показатель накопительного сдвига макета (CLS – Cumulative Layout Shift) помогает решить эту проблему, измеряя, как часто это происходит у реальных пользователей.
Что такое CLS?
Cumulative Layout Shift измеряет общую сумму всех индивидуальных оценок сдвига макета для каждого неожиданного сдвига макета, который происходит в течение всего времени жизни страницы.
Сдвиг макета происходит каждый раз, когда видимый элемент изменяет свое положение от одного кадра отрисовки к другому (см. ниже подробности о том, как рассчитываются индивидуальные оценки сдвига макета).
Что для 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).
Инструменты
В полевых условиях
- Chrome User Experience Report
- PageSpeed Insights
- Search Console (Core Web Vitals report)
web-vitals
JavaScript library
В лабораторных условиях
Измерение 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 анимации переходов вместо изменения свойств, запускающих изменение макета. Надо анимировать переходы таким образом, чтобы обеспечить контекст и непрерывность от состояния к состоянию.
Optimize Cumulative Layout Shift
Cumulative Layout Shift (CLS).