Запобігання кумулятивному зсуву макету за допомогою відео-фонів у 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 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 впливають на розмір пакету та кешування.