Обсудить задачу

Почему показатели качества сайта "прыгают" и как это исправить

Показатели качества сайта в 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.

Причина оказалась в трёх независимых источниках сдвига одновременно:

  1. Промо-баннер о скидке подгружался через JavaScript спустя 800 миллисекунд и вставлялся над hero-блоком, раздвигая всю страницу вниз
  2. Hero-изображение не имело атрибутов width и height
  3. Кастомный шрифт заголовков подключался без 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 не идут.

Показывать их можно и нужно - проблема не в самом баннере, а в том, как он встраивается в макет. Если позиционировать баннер поверх контента через 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. Найдём конкретный элемент, который сдвигает макет, и предложим фикс без изменения дизайна.

Ещё статьи

Все →