Как исправить проблемы с кумулятивным смещением макета (CLS)
Опубликовано: 2022-03-10Кумулятивный сдвиг макета (CLS) пытается измерить эти резкие движения страницы по мере того, как новый контент — будь то изображения, реклама или что-то еще — вступает в игру позже, чем остальная часть страницы. Он вычисляет оценку на основе того, какая часть страницы неожиданно перемещается и как часто. Эти изменения содержания очень раздражают, заставляя вас терять свое место в статье, которую вы начали читать, или, что еще хуже, заставляя нажимать не ту кнопку!
В этой статье я собираюсь обсудить некоторые шаблоны внешнего интерфейса для уменьшения CLS . Я не буду слишком много говорить об измерении CLS, так как я уже рассказывал об этом в предыдущей статье. Я также не буду слишком много говорить о механике расчета CLS: у Google есть неплохая документация по этому поводу, и «Почти полное руководство по кумулятивному смещению макета» Джесс Пек также является потрясающим глубоким погружением в это. Тем не менее, я дам небольшую предысторию, необходимую для понимания некоторых методов.
Чем отличается CLS
CLS, на мой взгляд, является наиболее интересным из Core Web Vitals, отчасти потому, что мы никогда раньше не измеряли и не оптимизировали его. Таким образом, часто требуются новые методы и способы мышления, чтобы попытаться оптимизировать его. Это совершенно другой зверь по сравнению с двумя другими Core Web Vitals.
Кратко взглянув на два других Core Web Vitals, Largest Contentful Paint (LCP) работает точно так, как следует из его названия, и представляет собой скорее поворот предыдущих показателей загрузки, которые измеряют, насколько быстро загружается страница. Да, мы изменили то, как мы определяли взаимодействие с пользователем при загрузке страницы, чтобы смотреть на скорость загрузки наиболее релевантного контента , но в основном это повторное использование старых методов обеспечения максимально быстрой загрузки контента. Как оптимизировать LCP должно быть относительно хорошо понятной проблемой для большинства веб-страниц.
Задержка первого ввода (FID) измеряет любые задержки во взаимодействиях и, по-видимому, не является проблемой для большинства сайтов. Оптимизация обычно заключается в очистке (или сокращении!) вашего JavaScript и обычно зависит от сайта. Это не значит, что решать проблемы с этими двумя метриками легко, но это достаточно хорошо понятные проблемы.
Одной из причин отличия CLS является то, что он измеряется временем существования страницы — это «кумулятивная» часть имени! Два других Core Web Vitals останавливаются после обнаружения основного компонента на странице после загрузки (для LCP) или при первом взаимодействии (для FID). Это означает, что наши традиционные лабораторные инструменты, такие как Lighthouse, часто не полностью отражают CLS, поскольку они вычисляют только CLS начальной нагрузки. В реальной жизни пользователь будет прокручивать страницу вниз и может получить больше контента, вызывая больше сдвигов.
CLS также является чем-то вроде искусственного числа, которое рассчитывается на основе того, какая часть страницы перемещается и как часто. В то время как LCP и FID измеряются в миллисекундах, CLS представляет собой безразмерное число , выводимое в результате сложного вычисления. Мы хотим, чтобы страница была 0.1 или ниже, чтобы пройти этот Core Web Vital. Все, что выше 0,25, считается «плохим».
Сдвиги, вызванные взаимодействием с пользователем, не учитываются . Это определяется как в пределах 500 мс от определенного набора взаимодействий пользователя, хотя события указателя и прокрутка исключаются. Предполагается, что пользователь, нажимающий кнопку, может ожидать появления содержимого, например, путем развертывания свернутого раздела.
CLS предназначен для измерения неожиданных сдвигов . Прокрутка не должна заставлять контент перемещаться, если страница построена оптимально, и аналогичным образом наведение курсора на изображение продукта, например, для получения увеличенной версии, также не должно вызывать прыжки другого контента. Но, конечно, есть исключения, и этим сайтам нужно подумать, как на это реагировать.
CLS также постоянно развивается, в него вносятся изменения и исправления ошибок. Только что было объявлено о более крупном изменении, которое должно дать некоторую передышку долгоживущим страницам, таким как одностраничные приложения (SPA) и страницы с бесконечной прокруткой, которые, по мнению многих, были несправедливо наказаны в CLS. Вместо того, чтобы накапливать сдвиги за все время страницы для расчета оценки CLS, как это делалось до сих пор, оценка будет рассчитываться на основе наибольшего набора сдвигов в пределах определенного временного окна.
Это означает, что если у вас есть три фрагмента CLS со значениями 0,05, 0,06 и 0,04, то раньше это значение было бы зарегистрировано как 0,15 (т. е. превышение «хорошего» предела 0,1), тогда как теперь оно будет оцениваться как 0,06. Он по-прежнему является кумулятивным в том смысле, что оценка может состоять из отдельных сдвигов в течение этого периода времени (т. е. если этот показатель CLS 0,06 был вызван тремя отдельными сдвигами по 0,02), но он больше не суммируется за все время жизни страницы. .
Говоря, что если вы устраните причины этого сдвига на 0,06, то ваш CLS будет указан как следующий по величине (0,05), поэтому он по-прежнему просматривает все сдвиги за время существования страницы — он просто выбирает отчет только самый большой из них - оценка CLS.
С этим кратким введением в некоторые методологии CLS, давайте перейдем к некоторым решениям ! Все эти методы в основном включают в себя выделение нужного объема пространства перед загрузкой дополнительного контента — будь то медиа или контент, внедренный JavaScript, но для этого веб-разработчики могут использовать несколько различных вариантов.
Установите ширину и высоту изображений и iFrames
Я уже писал об этом раньше, но одна из самых простых вещей, которые вы можете сделать, чтобы уменьшить CLS, — это убедиться, что у вас установлены атрибуты width
и height
для ваших изображений . Без них изображение приведет к смещению последующего контента, чтобы освободить место для него после загрузки:
Это просто вопрос изменения разметки изображения:
<img src="hero_image.jpg" alt="...">
К:
<img src="hero_image.jpg" alt="..." width="400" height="400">
Вы можете узнать размеры изображения, открыв DevTools и наведя указатель мыши на элемент (или коснувшись его).
Я советую использовать внутренний размер (который является фактическим размером источника изображения), и браузер затем уменьшит их до отображаемого размера, когда вы используете CSS для их изменения.
Подсказка : если, как и я, вы не можете вспомнить, является ли это шириной и высотой или высотой и шириной, думайте об этом как о координатах X и Y, поэтому, как и X, ширина всегда указывается первой.
Если у вас есть адаптивные изображения и вы используете CSS для изменения размеров изображения (например, чтобы ограничить его max-width
100% от размера экрана), то эти атрибуты можно использовать для расчета height
— при условии, что вы не забудете переопределить это, чтобы auto
в вашем CSS:
img { max-width: 100%; height: auto; }
Все современные браузеры поддерживают это сейчас, хотя до недавнего времени, как описано в моей статье. Это также работает для элементов <picture>
и изображений srcset
(устанавливайте width
и height
в резервном элементе img
), но пока не для изображений с другим соотношением сторон — над этим работают, и до тех пор вы все равно должны устанавливать width
и height
. так как любые значения будут лучше, чем значения по умолчанию 0
на 0
!
Это также работает с изображениями с отложенной загрузкой (хотя Safari пока не поддерживает отложенную загрузку по умолчанию).
Новое свойство CSS aspect-ratio
Приведенный выше метод расчета width
и height
для расчета высоты адаптивных изображений может быть распространен на другие элементы с помощью нового свойства 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
на основе атрибутов элемента, используя aspect-ratio: attr(width) / attr(height);
но, к сожалению, это еще не поддерживается.
Или вы даже можете использовать aspect-ratio
для элемента <div>
для своего рода пользовательского элемента управления, который вы создаете, чтобы сделать его отзывчивым:
#my-square-custom-control { max-width: 100%; height: auto; width: 500px; aspect-ratio: 1; }
<div></div>
Для тех браузеров, которые не поддерживают aspect-ratio
, вы можете использовать старый хак padding-bottom, но с простотой нового aspect-ratio
и широкой поддержкой (особенно после перехода от Safari Technical Preview к обычному Safari) это трудно оправдать этот старый метод.
Chrome — единственный браузер, который возвращает CLS в Google и поддерживает aspect-ratio
, что решит ваши проблемы с CLS с точки зрения Core Web Vitals. Мне не нравится отдавать приоритет метрикам, а не пользователям, но тот факт, что в других браузерах Chromium и Firefox это есть, и Safari, надеюсь, скоро появится, и что это прогрессивное улучшение, означает, что я бы сказал, что мы находимся в точке, где мы можно оставить хак с отступами и написать более чистый код.
Свободно используйте 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
, поскольку поле считается частью этого элемента, поэтому при его удалении будет казаться, что оно смещается (даже если на самом деле он не перемещается на экране). Однако, по крайней мере, содержимое под ним не будет смещено, поэтому следует уменьшить CLS.
Кроме того, вы можете использовать ::before
, чтобы добавить пробел, чтобы избежать сдвига элемента main-content
:
.main-content:first-child::before { content: ''; min-height: 20px; display: block; }
Но, честно говоря, лучшее решение — иметь div
в HTML и использовать для него min-height
.
Проверить резервные элементы
Мне нравится использовать прогрессивное улучшение для предоставления базового веб-сайта, даже без JavaScript, где это возможно. К сожалению, это недавно застало меня на одном сайте, который я поддерживаю, когда резервная версия без JavaScript отличалась от того, когда включился JavaScript.
Проблема возникла из-за кнопки меню «Оглавление» в заголовке. Перед запуском JavaScript это простая ссылка, оформленная в виде кнопки, которая ведет на страницу оглавления. Как только запускается JavaScript, оно становится динамическим меню , позволяющим вам переходить непосредственно на любую страницу, на которую вы хотите перейти с этой страницы.
Я использовал семантические элементы и поэтому использовал элемент привязки ( <a href="#table-of-contents">
) для резервной ссылки, но заменил его <button>
для динамического меню на основе JavaScript. Они были оформлены так, чтобы выглядеть одинаково, но резервная ссылка была на пару пикселей меньше, чем кнопка!
Это было так мало, а JavaScript обычно запускался так быстро, что я не заметил, как он был отключен. Однако Chrome заметил это при расчете CLS и, поскольку это было в шапке, сдвинул всю страницу вниз на пару пикселей. Так что это сильно повлияло на оценку CLS — достаточно, чтобы все наши страницы попали в категорию «Нуждается в улучшении».
Это была ошибка с моей стороны, и исправление заключалось в том, чтобы просто синхронизировать два элемента (это также можно было исправить, установив min-height
заголовка, как обсуждалось выше), но это немного смутило меня. Я уверен, что я не единственный, кто допустил эту ошибку, поэтому помните, как страница отображается без JavaScript. Не думаете, что ваши пользователи отключают JavaScript? Все ваши пользователи не используют JS, пока они загружают ваш 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, что было более сложным, но эта опция, которая должна появиться очень скоро, может, наконец, дать нам более простое решение, которое с большей вероятностью наберет обороты. См. мою предыдущую статью на эту тему для получения более подробной информации об этом предстоящем нововведении и других ресурсах по этому вопросу.
Исходные шаблоны для страниц, отображаемых на стороне клиента
Многие страницы, отображаемые на стороне клиента, или одностраничные приложения отображают исходную базовую страницу, используя только HTML и CSS, а затем «увлажняют» шаблон после загрузки и выполнения JavaScript.
Эти исходные шаблоны легко могут не синхронизироваться с версией JavaScript, поскольку новые компоненты и функции добавляются в приложение в JavaScript, но не добавляются в исходный шаблон HTML, который отображается первым. Затем это вызывает CLS, когда эти компоненты внедряются JavaScript.
Поэтому просмотрите все свои первоначальные шаблоны , чтобы убедиться, что они все еще являются хорошими первоначальными заполнителями. И если первоначальный шаблон состоит из пустых <div>
, то используйте описанные выше методы, чтобы убедиться, что они имеют соответствующий размер, чтобы избежать каких-либо сдвигов.
Кроме того, начальный div
, который вводится вместе с приложением, должен иметь min-height
, чтобы избежать рендеринга с нулевой высотой до того, как будет вставлен исходный шаблон.
<div></div>
Пока min-height
больше, чем у большинства видовых экранов , это позволит избежать CLS, например, для нижнего колонтитула веб-сайта. CLS измеряется только тогда, когда он находится в области просмотра и поэтому влияет на пользователя. По умолчанию пустой элемент div
имеет высоту 0 пикселей, поэтому задайте для него min-height
, которая будет ближе к реальной высоте при загрузке приложения.
Гарантируйте, что взаимодействие пользователя завершится в течение 500 мс
Взаимодействия пользователей, которые приводят к смещению контента, исключаются из оценок CLS. Они ограничены 500 мс после взаимодействия. Поэтому, если вы нажмете кнопку и выполните сложную обработку, которая занимает более 500 мс, а затем отобразите какой-то новый контент, то ваша оценка CLS пострадает.
Вы можете увидеть, была ли смена исключена в Chrome DevTools , используя вкладку «Производительность» для записи страницы, а затем найдя смены, как показано на следующем снимке экрана. Откройте DevTools, перейдите на очень пугающую (но очень полезную, как только вы освоитесь!) вкладку « Производительность », а затем нажмите кнопку записи в левом верхнем углу (обведена кружком на изображении ниже), взаимодействуйте со своей страницей и остановите запись один раз. полный.
Вы увидите диафильм страницы, на которой я загрузил некоторые комментарии к другой статье Smashing Magazine, поэтому в той части, которую я обвел кружком, вы можете почти разглядеть загрузку комментариев и смещение красного нижнего колонтитула за пределы экрана. Далее на вкладке « Производительность » под строкой « Опыт » Chrome будет отображать красновато-розоватую рамку для каждой смены, и когда вы нажмете на нее, вы получите более подробную информацию на вкладке « Сводка » ниже.
Здесь вы можете видеть, что мы получили огромную оценку 0,3359 — намного больше порога 0,1, ниже которого мы стремимся быть, но совокупная оценка не включает это, потому что для параметра « Недавние входные данные » установлено значение «Использует».
Обеспечение взаимодействий только сдвига содержимого в пределах 500 мс граничит с тем, что пытается измерить первая задержка ввода, но бывают случаи, когда пользователь может видеть, что ввод имел эффект (например, отображается счетчик загрузки), поэтому FID хорош, но содержимое может не добавляться на страницу до тех пор, пока не будет превышено ограничение в 500 мс, поэтому CLS — это плохо.
В идеале, все взаимодействие будет завершено в течение 500 мс, но вы можете сделать некоторые вещи, чтобы выделить необходимое пространство , используя описанные выше методы, пока идет обработка, так что, если это займет больше магических 500 мс, тогда вы уже справился со сменой и поэтому не будет наказан за это. Это особенно полезно при получении содержимого из сети, которое может изменяться и находится вне вашего контроля.
Другими элементами, на которые следует обратить внимание, являются анимации , которые занимают более 500 мс и поэтому могут повлиять на CLS. Хотя это может показаться немного ограничительным, цель CLS состоит не в том, чтобы ограничить «удовольствие», а в том, чтобы установить разумные ожидания пользователя, и я не думаю, что нереально ожидать, что это займет 500 мс или меньше. Но если вы не согласны или у вас есть вариант использования, который они, возможно, не рассматривали, команда Chrome открыта для отзывов по этому поводу.
Синхронный JavaScript
Последний метод, который я собираюсь обсудить, немного противоречив, поскольку он противоречит известным советам по производительности в Интернете, но в определенных ситуациях он может быть единственным методом. По сути, если у вас есть контент, который, как вы знаете, вызовет сдвиги, то одно из решений, позволяющих избежать сдвигов, — не отображать его до тех пор, пока он не стабилизируется!
Приведенный ниже HTML сначала скроет div
, затем загрузит код JavaScript, блокирующий рендеринг, чтобы заполнить div
, а затем отобразит его. Поскольку JavaScript блокирует рендеринг, ничего ниже этого не будет отображаться (включая второй блок style
, чтобы отобразить его), и поэтому никаких сдвигов не будет.
<style> .cls-inducing-div { display: none; } </style> <div class="cls-inducing-div"></div> <script> ... </script> <style> .cls-inducing-div { display: block; } </style>
С помощью этого метода важно встроить CSS в HTML , поэтому он применяется по порядку. Альтернативой является отображение контента с помощью самого JavaScript, но что мне нравится в вышеописанном методе, так это то, что он по-прежнему показывает контент, даже если JavaScript не работает или отключен браузером.
Этот метод также можно применять с внешним JavaScript, но это вызовет большую задержку, чем встроенный script
, поскольку внешний 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"
(которые по умолчанию defer
-ed) на JavaScript специально, чтобы избежать блокировки render , тогда как здесь мы делаем наоборот! Однако, если содержимое нельзя предопределить и оно вызовет резкие сдвиги, то нет смысла рендерить его раньше.
Я использовал эту технику для баннера cookie , который загружался вверху страницы и перемещал содержимое вниз:
Это требовало чтения файла cookie, чтобы увидеть, отображать ли баннер файла cookie или нет, и, хотя это можно было выполнить на стороне сервера, это был статический сайт без возможности динамически изменять возвращаемый HTML.
Баннеры cookie могут быть реализованы по-разному, чтобы избежать CLS. Например, размещая их внизу страницы или накладывая их поверх содержимого, а не сдвигая содержимое вниз. Мы предпочли, чтобы контент находился вверху страницы, поэтому пришлось использовать этот прием, чтобы избежать сдвигов. Существуют различные другие предупреждения и баннеры, которые владельцы сайтов могут предпочесть размещать вверху страницы по разным причинам.
Я также использовал эту технику на другой странице, где JavaScript перемещает содержимое в «главные» и «боковые» столбцы (по причинам, которые я не буду вдаваться, было невозможно правильно сконструировать это на стороне сервера HTML). Снова скрытие содержимого, пока JavaScript не переупорядочит содержимое, и только затем показ его, избегает проблем с CLS, которые снижают оценку CLS для этих страниц. И снова содержимое автоматически отображается, даже если JavaScript по какой-то причине не запускается, и показывается несдвинутое содержимое.
Использование этого метода может повлиять на другие показатели (в частности, на LCP, а также на первую отрисовку содержимого), поскольку вы откладываете рендеринг, а также потенциально блокируете предварительный загрузчик браузеров, но это еще один инструмент, который следует учитывать в тех случаях, когда не существует другого варианта.
Заключение
Совокупное смещение макета вызвано изменением размеров контента или добавлением нового контента на страницу с задержкой выполнения JavaScript. В этом посте мы обсудили различные советы и приемы, чтобы избежать этого. Я рад, что Core Web Vitals обратили внимание на эту раздражающую проблему — слишком долго мы, веб-разработчики (и я определенно включаю себя в это) игнорировали эту проблему.
Очистка моих собственных веб-сайтов привела к лучшему опыту для всех посетителей. Я призываю вас также обратить внимание на свои проблемы с CLS , и, надеюсь, некоторые из этих советов будут вам полезны, когда вы это сделаете. Кто знает, возможно, вам даже удастся достичь неуловимого 0 баллов CLS для всех ваших страниц!
Дополнительные ресурсы
- Статьи Core Web Vitals здесь, в журнале Smashing Magazine, в том числе мои статьи о настройке ширины и высоты изображений, измерении Core Web Vitals и дескрипторах шрифтов CSS.
- Документация Google Core Web Vitals, включая их страницу в CLS.
- Более подробная информация о недавнем изменении CLS, а затем это изменение начало обновляться в различных инструментах Google.
- Журнал изменений CLS с подробным описанием изменений в каждой версии Chrome.
- Почти полное руководство по кумулятивному смещению макета Джесс Пек.
- Накопительное смещение макета: Измерьте и избегайте визуальной нестабильности, Каролина Щур.
- Генератор Layout Shift GIF, помогающий создавать общие демонстрации CLS.