Показатели качества сайта в PageSpeed Insights и Google Search Console скачут между визитами из-за одной метрики - CLS (Cumulative Layout Shift), совокупного сдвига макета. Оптимизация CLS в большинстве случаев сводится к одному принципу: браузер должен заранее знать размер и место каждого элемента, чтобы ничего не двигалось после того, как пользователь это увидел. Дальше - конкретные причины сдвигов, рабочие CSS-фиксы и как проверить, что именно “прыгает” на конкретной странице.
По данным CrUX за май 2026 года, которые приводит в обзоре DigitalApplied, CLS - метрика Core Web Vitals с самым высоким процентом прохождения: 81,3% сайтов укладываются в порог 0.1, против 68,6% для LCP и 86,6% для INP. Это создаёт обманчивое чувство защищённости: раз CLS “обычно и так проходит”, про неё забывают в первую очередь, когда PageSpeed показывает нестабильный балл от визита к визиту на одной и той же странице.
В проектах аудита сайтов мы регулярно видим один и тот же паттерн: клиент присылает скриншот PageSpeed с оценкой 92, а на следующий день - 61 для той же страницы без единой правки в коде. Первая мысль обычно - “сервер тормозит” или “Google глючит”. На деле почти всегда виновата не скорость, а нестабильность одного и того же элемента: он сдвигается по-разному в зависимости от того, что успело или не успело загрузиться раньше него.
Дальше разберём, какие конкретно элементы вызывают сдвиг, как зафиксировать их в CSS без изменения дизайна, и как отличить реальную проблему макета от случайного шума измерений.
Для бизнеса это не абстрактная метрика в отчёте. Каждый сдвиг макета - это доля секунды, когда пользователь целится в одну кнопку, а под курсором на её месте оказывается другая: баннер подписки вместо кнопки “Купить”, реклама вместо ссылки на статью. Промах по клику раздражает и выглядит непрофессионально, а для интернет-магазина или лендинга с оплатой это прямая потеря конверсии, а не только позиций в поиске.
CLS (Cumulative Layout Shift): метрика Core Web Vitals, которая суммирует все неожиданные сдвиги видимых элементов страницы за время её жизни. Значение ниже 0.1 считается хорошим, от 0.1 до 0.25 - зоной для улучшения, выше 0.25 - плохим.
Как измерить сдвиг, прежде чем оптимизировать CLS
Перед тем как чинить сдвиг, нужно точно узнать, какой элемент его вызывает - и здесь важно не перепутать полевые данные с лабораторными.
Полевые данные (CrUX) собираются из реального трафика Chrome за скользящее окно 28 дней - именно их видит Google Search Console в разделе Core Web Vitals, и именно они влияют на ранжирование. Lighthouse в PageSpeed Insights или DevTools - лабораторный тест: один синтетический прогон в фиксированных условиях, который может не совпадать с тем, что видят реальные пользователи на разных устройствах и соединениях.
Разница объясняет тот самый скачущий балл: если PageSpeed на одном прогоне показывает 92, а на другом 61, скорее всего лабораторный тест каждый раз по-разному попадает в момент, когда на странице что-то сдвигается - реклама, баннер, шрифт. Полевые данные CrUX сглаживают это за счёт тысяч визитов, но именно поэтому правда видна не сразу, а через недели.
Для отладки конкретного сдвига откройте вкладку Performance в Chrome DevTools, запишите загрузку страницы и найдите на треке Experience записи Layout Shift. Клик по записи показывает, какой именно элемент сдвинулся, на сколько пикселей и в какой момент - это быстрее, чем гадать по коду.
Как найти проблемный элемент через DevTools
Откройте DevTools (F12) -> вкладка Performance -> Record. Перезагрузите страницу, дождитесь полной загрузки, остановите запись. На треке Experience найдите красные блоки Layout Shift - клик по блоку раскрывает Summary с элементом, который сдвинулся, и величиной сдвига в конкретный момент времени.
Второй способ - расширение Web Vitals от Google или библиотека web-vitals в продакшене: она отправляет реальные значения CLS с устройств пользователей в вашу аналитику, включая ссылку на конкретный DOM-элемент.
Изображения, реклама и виджеты без зарезервированного места
Самая частая причина высокого CLS - элемент, для которого браузер не знает финальный размер до момента, пока контент не загрузится полностью.
Если у тега <img> не заданы width и height (или CSS-свойство aspect-ratio), браузер не может вычислить высоту блока заранее и оставляет её нулевой до полной загрузки файла. Когда картинка приходит, весь контент ниже сдвигается вниз ровно на её высоту. То же самое происходит с видео, iframe-встройками (карты, видео с YouTube, виджеты бронирования) и рекламными блоками.
Пример: явные размеры и aspect-ratio
<img src="/hero.webp" width="1200" height="630" alt="Описание">
.embed-container {
aspect-ratio: 16 / 9;
width: 100%;
}
Оба подхода решают одну задачу - браузер резервирует место под элемент сразу при разборе HTML, до того как файл скачается. aspect-ratio удобнее для адаптивной вёрстки, где ширина меняется по брейкпоинтам, а высота должна пересчитываться пропорционально.
Вес и формат самого изображения (AVIF против WebP, srcset, приоритет загрузки) - отдельная тема, которая влияет на LCP, а не на CLS, и подробно разобрана в статье про оптимизацию LCP через изображения. Для CLS важны только зарезервированные размеры контейнера, а не вес файла.
Шрифты, которые сдвигают текст при подключении
Веб-шрифты вызывают сдвиг не потому что они тяжёлые, а потому что браузер вынужден один раз перерисовать текст, когда шрифт наконец загружается.
FOIT (Flash of Invisible Text) - браузер прячет текст полностью, пока кастомный шрифт не скачается, и показывает пустое место. FOUT (Flash of Unstyled Text) - браузер сразу показывает текст системным шрифтом, а затем заменяет его на кастомный. В обоих случаях, если у системного и кастомного шрифта разная ширина символов, замена сдвигает всё, что стоит после текста - абзацы, кнопки, соседние блоки.
CSS-свойство font-display управляет этим поведением. Значение swap показывает текст системным шрифтом сразу, но не устраняет сдвиг при замене. Значение optional - выбор для стабильности макета: если кастомный шрифт не успевает загрузиться за первые 100 миллисекунд, браузер до конца сессии показывает только fallback-шрифт, без замены и без сдвига.
Пример: font-display: optional с preload
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom.woff2") format("woff2");
font-display: optional;
}
<link rel="preload" as="font" href="/fonts/custom.woff2" type="font/woff2" crossorigin>
Сочетание preload и font-display: optional - самый надёжный способ гарантировать отсутствие сдвига от шрифтов: либо шрифт успевает загрузиться до первой отрисовки, либо страница остаётся на fallback-шрифте всю сессию.
Если полный отказ от кастомного шрифта при медленном соединении неприемлем для бренда, второй вариант - подобрать fallback-шрифт с похожими метриками ширины символов через size-adjust и связанные дескрипторы @font-face, чтобы разница в ширине текста после замены была визуально незаметной.
Контент, который вставляется поверх уже показанного
Отдельная категория сдвигов - не про размер элемента, а про момент, когда он появляется: если новый блок вставляется через JavaScript над уже отрисованным контентом, всё ниже уезжает вниз.
Классический пример - баннер согласия на cookie, который появляется через секунду после загрузки страницы и раздвигает шапку сайта. Похожая ситуация - рекламный блок, который подгружается асинхронно и вставляется в верхнюю часть страницы, или promo-уведомление над основным контентом.
Рабочий приём - резервировать место под такой блок заранее через CSS, даже если сам контент ещё не готов: пустой контейнер с фиксированной min-height держит layout стабильным, а после загрузки контент просто заполняет уже выделенное место, ничего не раздвигая. Второй вариант для баннеров и cookie-уведомлений - позиционировать их поверх контента через position: fixed или position: sticky в нижней части экрана, а не встраивать в поток документа: тогда они физически не могут сдвинуть остальной макет.
Анимации, которые двигают макет, а не просто визуально едут
Не любая анимация в CLS учитывается - разница в том, какое CSS-свойство её запускает.
Анимации через top, left, width, height или margin заставляют браузер каждый кадр пересчитывать позицию и размеры элементов в потоке документа - это ровно то, что метрика CLS фиксирует как сдвиг. Анимации через transform и opacity браузер рисует на отдельном композитном слое, не трогая раскладку документа вообще - визуально элемент двигается точно так же, а в счёт CLS это не попадает.
Пример: анимация через transform вместо top
/* Плохо: анимация через top пересчитывает layout каждый кадр */
.banner {
position: relative;
animation: slide-in-bad 0.3s ease-out;
}
@keyframes slide-in-bad {
from { top: -100px; }
to { top: 0; }
}
/* Лучше: тот же визуальный эффект через transform */
.banner {
animation: slide-in-good 0.3s ease-out;
}
@keyframes slide-in-good {
from { transform: translateY(-100px); }
to { transform: translateY(0); }
}
Важная деталь методологии: сдвиги, которые происходят в течение 500 миллисекунд после клика, тапа или нажатия клавиши пользователем, в CLS не засчитываются - Google трактует их как ожидаемую реакцию на действие, а не как неожиданный сдвиг. А вот прокрутка и pinch-жесты под это исключение не попадают: анимация, которая сдвигает элементы во время скролла через layout-свойства, добавляется к счёту CLS так же, как любой другой сдвиг.
Как CLS упал с 0.34 до 0.03 в реальном проекте
В типовом проекте аудита лендинга с формой заявки PageSpeed показывал нестабильный результат: от 88 до 54 на разных прогонах одной и той же страницы, при в целом быстром LCP и хорошем INP.
Причина оказалась в трёх независимых источниках сдвига одновременно:
- Промо-баннер о скидке подгружался через JavaScript спустя 800 миллисекунд и вставлялся над hero-блоком, раздвигая всю страницу вниз
- Hero-изображение не имело атрибутов
widthиheight - Кастомный шрифт заголовков подключался без
font-display, из-за чего браузер по умолчанию использовалblock- текст перерисовывался с заметной разницей в ширине символов
Что изменили за несколько дней работы: зарезервировали контейнер под промо-баннер фиксированной высотой ещё до его загрузки, задали width и height всем изображениям выше первого экрана, добавили font-display: optional с preload для шрифта заголовков.
Результат по полевым данным CrUX через 28 дней после деплоя:
- CLS: 0.34 (плохо) -> 0.03 (хорошо)
- PageSpeed стабилизировался в диапазоне 90-94 на всех прогонах вместо разброса 54-88
- LCP и INP не изменились, что подтверждает: проблема была локализована именно в стабильности макета, а не в скорости
Для кого это особенно актуально
Нестабильный CLS чаще всего встречается на сайтах, где контент кроме основной вёрстки подгружается динамически: интернет-магазины с рекламными блоками и промо-баннерами, лендинги с cookie-уведомлениями и формами подписки, сайты на WordPress или Tilda с несколькими плагинами, каждый из которых добавляет свой скрипт независимо от остальных.
Отдельная категория - компании, которые недавно сменили тему WordPress, добавили новый плагин слайдера или подключили виджет чата: любое из этих изменений может незаметно вернуть CLS в красную зону, даже если раньше метрика была в норме.
Если ваш сайт построен на WordPress, специфичные для этой платформы причины - конфликты плагинов, Elementor, Gutenberg-блоки без размеров - разобраны отдельно в статье про Core Web Vitals на WordPress. Здесь фокус был на причинах, общих для любого стека: WordPress, Astro, Next.js или самописной вёрстки. Третья метрика Core Web Vitals - задержка отклика на действия пользователя - подробно разобрана в статье про оптимизацию INP.
Разовая правка снимает симптом, но новый плагин, баннер от маркетинга или обновление темы легко возвращают CLS обратно. Для сайтов с частыми изменениями это стоит держать под регулярным контролем в рамках поддержки сайта, а не проверять раз в год.
Часто задаваемые вопросы
Почему PageSpeed показывает разные баллы для одной и той же страницы?
Чаще всего это признак нестабильного CLS: если реклама, баннер или шрифт сдвигают макет в разное время на разных прогонах, лабораторный тест Lighthouse каждый раз попадает в разный момент сдвига и выдаёт разную оценку. Стабильный балл без разброса - хороший косвенный признак того, что сдвигов на странице нет вообще, а не просто небольшой CLS.
Считается ли анимация плохой для CLS, если элемент сдвигается ещё до того, как его увидел пользователь?
Да, в CLS засчитываются сдвиги видимых элементов независимо от того, заметил их пользователь физически или нет - метрика оценивает область экрана, которая изменилась, а не факт восприятия. Исключение - сдвиги в течение 500 миллисекунд после клика, тапа или нажатия клавиши: они считаются ожидаемой реакцией интерфейса и в счёт CLS не идут.
Что делать с cookie-баннером или рекламой, если их нельзя не показывать?
Показывать их можно и нужно - проблема не в самом баннере, а в том, как он встраивается в макет. Если позиционировать баннер поверх контента через position: fixed в нижней части экрана вместо того чтобы вставлять его в обычный поток документа, он не будет физически раздвигать остальную страницу и не попадёт в счёт CLS.
Сколько ждать, чтобы увидеть эффект исправлений в отчётах?
Lighthouse и PageSpeed Insights показывают эффект сразу после деплоя, потому что это лабораторный тест одного прогона. Полевые данные CrUX в Google Search Console обновляются на основе скользящего окна 28 дней, поэтому окончательную картину по реальным пользователям стоит проверять примерно через месяц после изменений.
Отличаются ли причины CLS на WordPress и на headless-стеке вроде Astro или Next.js?
Базовые причины одинаковы везде - CLS не зависит от того, как отрисован HTML, а зависит от того, зарезервировано ли место под элементы заранее. На WordPress источник чаще всего в теме, page builder или конфликте нескольких плагинов, которые независимо друг от друга вставляют скрипты. На headless-стеке вроде Astro или Next.js таких скрытых источников меньше, потому что вёрстка полностью под контролем команды, но ошибка та же самая: забытый width и height у изображения или анимация через top вместо transform.
Что стоит проверить в первую очередь:
- Открыть вкладку Performance в Chrome DevTools и найти реальные записи
Layout Shift, а не гадать по внешнему виду страницы - Задать
widthиheight(илиaspect-ratio) всем изображениям, видео и iframe-встройкам выше первого экрана - Добавить
font-display: optionalсpreloadдля кастомных шрифтов, которые меняют текст после загрузки - Проверить, не встраивается ли динамический контент - баннеры, реклама, cookie-уведомления - поверх уже показанной страницы
Если PageSpeed на вашем сайте скачет между прогонами, а реальные пользователи жалуются на нестабильный интерфейс - опишите задачу команде Exceltic.dev. Найдём конкретный элемент, который сдвигает макет, и предложим фикс без изменения дизайна.