Предотвращение кумулятивного смещения макета с использованием видео-фона в Next.js
Изучите практические техники CSS и формирования макета, чтобы предотвратить возникновение эффекта Cumulative Layout Shift при использовании видео на фоне в Next.js на разных устройствах и при различной скорости сети.
Существует особый тип ошибок фронтенда, который остается незаметным во время разработки в идеальных условиях, но как только скорость сети снижается, видео на фоне, которое вы добавили, заставляет страницу скачкообразно менять размеры, словно это сцена из хаотичного ситкома.
Поздравляем: вы только что столкнулись с явлением Cumulative Layout Shift.
Видео-фоны легко остаются незамеченными при проверке, потому что они находятся на фоне, нормально работают при высокой скорости соединения, и инженеры часто рассматривают видео как исключительно визуальный слой, а не элемент, участвующий в формировании макета страницы.
Однако браузер не разделяет наши предположения. Когда размер элемента неизвестен в момент первоначальной отрисовки, браузер вынужден делать предположения. И именно эти предположения — то, чего вы точно не хотите, чтобы они влияли на принятие решений по макету.
Теперь, когда эти обстоятельства ясны, давайте перейдем к техническим деталям.
Cumulative Layout Shift показывает, насколько неожиданно смещается видимый контент. Самый простой способ избежать этого — задать элементам медиа предсказуемые, известные размеры до того, как их ресурсы полностью загружены. Установка явных значений ширины и высоты или использование свойства CSS aspect-ratio позволяет браузеру зарезервировать это пространство заранее в процессе расчёта макета.
Проще говоря: зарезервируйте место для элемента до того, как он фактически отрендерится.
Для раздела с главным видео хорошей отправной точкой будет контейнер с фиксированными размерами, а не позволение самой тегу <video> определять размер области с главным контентом.
export function VideoHero() {
return (
<section className="relative min-h-[70svh] overflow-hidden">
<video
className="absolute inset-0 h-full w-full object-cover"
autoPlay
muted
loop
playsInline
preload="metadata"
aria-hidden="true"
>
<source src="/hero-video.mp4" type="video/mp4" />
</video>
<div className="relative z-10 mx-auto max-w-6xl px-6 py-24">
<h1 className="text-5xl font-bold text-white">
Build products people remember.
</h1>
</div>
</section>
);
}
Критически важной здесь является совсем не маркировка видео.
Это одна-единственная строка:
min-height: 70svh;
Из-за этого раздел с героем уже имеет определенный визуальный формат до того, как начнется воспроизведение видео.
Само видео размещается абсолютно внутри этого контейнера, что означает, что у него нет возможности внезапно переместить содержимое под себя после завершения загрузки ресурса.
Именно это отличие полностью меняет уровень стабильности восприятия страницы.
Не позволяйте видео определять структуру вашего макета
Типичная проблематичная конфигурация выглядит так:
<video
src="/hero-video.mp4"
autoPlay
muted
loop
/>
Затем кто-то добавляет:
video {
width: 100%;
}
А позже удивляется, почему поведение страницы меняется в зависимости от качества соединения или размера экрана.
Проблема в том, что браузеру необходимо заранее знать размеры видео. Если видео является исключительно декоративным фоновым контентом, редко бывает веская причина для его участия в обычном потоке документа.
Вместо этого передайте ответственность за форматирование текста контейнеру-обёртке, тому div, который окружает элемент видео.
<div style="height: 100px; width: 100%">
<video
src="/hero-video.mp4"
autoPlay
muted
loop
/>
</div>
При такой структуре видео может вести себя как угодно внутри, но окружающий его div удерживает всё в пределах границ.
Используйте object-fit: cover для фонового видео
Как только видео помещается в абсолютные координаты, свойство object-fit: cover становится действительно полезным (редкий случай, когда свойство CSS работает именно так, как предусмотрено).
.heroVideo {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
Это позволяет видео полностью заполнить выделенную область без изменения размеров самого контейнера.
Стоит отметить небольшой компромисс: значение cover обрезает части видео, если соотношение сторон окна просмотра не совпадает со соотношением сторон исходного видео. Это разумная цена за такую функциональность.
Настоящая ошибка заключается в том, что приоритет отдается идеальному сохранению исходного видео в пикселях, тогда как на самом деле дизайн требует стабильной и предсказуемой структуры.
Используйте постер в качестве первого визуального состояния
Особенно эффективным способом является использование тщательно подобранного изображения-постера для элемента видео.
<video
className="absolute inset-0 h-full w-full object-cover"
autoPlay
muted
loop
playsInline
preload="metadata"
poster="/images/hero-poster.webp"
aria-hidden="true"
>
<source src="/videos/hero.mp4" type="video/mp4" />
</video>
Постер предоставляет посетителям что-то конкретное для просмотра во время загрузки самого видео. Выбирайте такое изображение очень тщательно, поскольку оно фактически служит вашим запасным дизайном.
Что ещё важнее, это означает, что ваша структура больше не зависит от мгновенной доступности видеоресурса.
Рассматривайте постер как надежное базовое состояние, а само видео — как дополнение в формате постепенного улучшения, подобно тому, как работают загрузчики-скелеты или индикаторы загрузки в других ситуациях.
Если видео загружается несколько секунд из-за медленного мобильного соединения, страница всё равно выглядит завершенной и корректной, а не сломанной или частично загруженной.
Внимание на подводные камни 100vh
Видео в полноэкранном режиме также сопряжено с ещё одной скрытой проблемой.
Раньше разработчики часто писали:
height: 100vh;
Мобильные браузеры усложняют ситуацию, поскольку высота видимой области просмотра меняется при появлении и исчезновении элементов интерфейса браузера (строка адреса, панели инструментов), поэтому 100vh ведёт себя не так, как ожидается.
Для макетов, которые должны хорошо адаптироваться к различным устройствам, обычно лучше использовать более современные единицы измерения области просмотра, такие как:
min-height: 100svh;
или, в зависимости от того, какую часть экрана нужно занять видео:
min-height: 80svh;
Кратко: предпочитайте 100dvh или 100svh вместо старого 100vh.
Избегайте отображения видео в размере для настольных ПК пользователям с мобильными устройствами
Даже страница с идеальным показателем CLS может казаться чрезмерно медленной, если само фоновое видео имеет огромный размер.
Именно здесь работа над производительностью становится более сложной.
Качественный кинематографический клип весом 12 МБ может отлично работать при быстром подключении к настольному ПК, но на мобильном устройстве через нестабильную сеть тот же файл превращается в настоящую проблему.
Для большинства лендинг-страниц целесообразно предоставлять отдельные видеофайлы для настольных и мобильных дисплеев.
В некоторых случаях правильным решением будет вообще не показывать видео определенным посетителям.
Учет предпочтений пользователя относительно снижения скорости воспроизведения — хороший пример этого:
const prefersReducedMotion =
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
В реальном React-приложении такая проверка должна находиться в клиентском компоненте, и её следует реализовывать тщательно, чтобы она не нарушала структуру HTML, отрендеренную на сервере.
Никогда не позволяйте видео определять макет
Это основной принцип, который следует иметь в виду на протяжении всей работы.
Структура вашей страницы должна корректно функционировать при такой последовательности действий:
Hero container
↓
Poster
↓
Video enhancement
и никогда не следует полагаться на этот вариант:
Video starts loading
↓
Browser discovers dimensions
↓
Hero changes height
↓
Everything below moves
↓
Lighthouse gets angry
Браузер должен уже понимать структуру макета до того, как появится более объемный медиаконтент.
Вот настоящее решение проблем CLS, вызванных видео.
Чтобы проверить это на практике, загрузите свой сайт через замедленное сетевое соединение с ограниченной пропускной способностью и посмотрите, как он будет вести себя.
Связанные статьи
- Откладывание побочных эффектов в Next.js с использованием API after() — Узнайте, как API after() в Next.js выполняет аналитику, логирование и фоновые задачи после отправки ответа, а также о его гарантиях, подводных камнях и компромиссах в обработке ошибок.
- Настройка новых механизмов разбиения на чанки в Turbopack в Next.js 16.3 — Практический обзор новой настройки turbopackChunking в Next.js 16.3 с объяснением того, как maxChunkCountPerGroup и generateComponentChunks влияют на размер пакета и кэширование.