- PVSM.RU - https://www.pvsm.ru -

Как я ускорил страницу с демо-видео: сжатие видео, автоплей и страница в 400 КБ вместо изначальных 3,1 МБ

Для главной страницы я сделал минутное видео с четырьмя главами: можно посмотреть всё демо или сразу перейти к интересующей функции. Плеер работает на обычном HTML <video> и небольшом JS-контроллере, без сторонних библиотек.

Сам ролик удалось сжать с 38 до 1,8 МБ. Но для быстрой загрузки страницы этого оказалось недостаточно: пришлось уменьшить картинку-заставку и отложить запуск видео. В итоге при открытии страница загружает около 400 КБ вместо 3,1 МБ, а видео - уже после первого действия посетителя.

В статье покажу код плеера, команды FFmpeg и результаты замеров. Объясню, почему preload="metadata" не спасает при автоплее, зачем возвращать видео к началу главы при включении звука и как добавить разметку таймкодов для Google.

Задача: показать четыре сценария на первом экране

Я Антон, разрабатываю менеджер буфера обмена для macOS Klipto.

На главной сайта нужно было показать четыре сценария/фичи: вставку скопированных значений в поля гугл таблицы (функция Stack), поиск по истории буфера, захват текста с экрана с переводом и работу с заметками.

Если показать их последовательно в ролике, то лишь небольшая часть досмотрит ролик и узнает обо всех фичах. Если разнести по странице - придётся рассчитывать, что люди будут скроллить.

При выборе компоновки я опирался на два исследования:

То есть рассчитывать, что человек обязательно проскроллит страницу или посмотрит всё демо, оснований мало.

Таймкоды, которые заодно объясняют продукт

Вместо кнопки "Посмотреть демо" с попапом как обычно делают на продуктовых сайтах я показываю видео прямо на первом экране. Под ним четыре карточки: название сценария/фичи, короткое описание и прогресс-бар. Каждая карточка работает как таймкод. Заинтересовала фича - нажимаешь на третью и переходишь к нужному фрагменту.

Как я ускорил страницу с демо-видео: сжатие видео, автоплей и страница в 400 КБ вместо изначальных 3,1 МБ - 1

X-идея - chunking, разбиении на смысловые части, которое описывает Nielsen Norman Group [3]. Четыре коротких фрагмента со знакомой по YouTube навигацией должны восприниматься легче, чем одно длинное демо. Внутри каждой карточки идет прогресс просмотра.

Названия глав можно быстро просканировать и перейти к интересному. Даже без просмотра они работают как список основных возможностей приложения.

Контроллер: метаданные, перемотка и прогресс

Сайт собран на Astro [4] и размещён на Cloudflare Pages [5]. Тег <video> сразу есть в HTML, а не создаётся через JavaScript. Поэтому браузер может начать загружать картинку-заставку, не дожидаясь выполнения скрипта.

У каждой кнопки есть время начала главы:

<button data-at="0">Paste in Sequence</button>
<button data-at="17">Clipboard</button>
<button data-at="30">Capture &amp; Translate</button>
<button data-at="43">Notes</button>

Для перемотки достаточно присвоить значение currentTime. Есть проблемы при клике сразу после открытия страницы: метаданные файла могут ещё не загрузиться. Поэтому выбранная карточка подсвечивается сразу, а перемотка откладывается до loadedmetadata.

Ниже сокращённый фрагмент контроллера. marks содержит таймкоды, activate() выделяет карточку, syncPlay() обновляет кнопку воспроизведения:

function seek(i) {
  if (v.readyState < 1) {
    v.addEventListener('loadedmetadata', () => seek(i), { once: true });
    activate(i);
    return;
  }

  v.currentTime = marks[i];
  activate(i);
  delete v.dataset.upaused;
  armed = true;
  v.play().catch(() => {});
  syncPlay();
}

Клик по карточке снимает ранее установленную пользователем паузу (если он ставил видео на паузу). Если браузер отклонит play(), остаётся обычная кнопка запуска.

Прогресс считается по реальному времени видео, а не по CSS-анимации заданной длительности:

const start = marks[i];
const end = marks[i + 1] ?? v.duration;
const fraction = (v.currentTime - start) / Math.max(0.001, end - start);
const percent = Math.max(0, Math.min(1, fraction)) * 100;

Во время воспроизведения контроллер обновляет активную главу и полосу через requestAnimationFrame. На паузе цикл останавливается, после перемотки состояние пересчитывается. Если видео буферизуется, прогресс не убегает вперёд.

Сжатие: почти половина битрейта уходила на звук

Исходный экспорт - 2560×1440, 37,9 МБ. Обычно я прогонял видео через два бесплатных браузерных инструмента: ezgif [6] для изменения размера и mp4compress [7] для дополнительного сжатия. На выходе получался файл 1280×720 размером 2,54 МБ. Его взял за точку сравнения

Разбор потоков показал, где ещё можно сократить размер:

ffprobe -v error -show_entries stream=codec_name,codec_type,bit_rate 
  -of default=noprint_wrappers=1 compressed.mp4
video: 172 kbps
audio: 133 kbps

На аудио приходилось около 43% суммарного битрейта потоков. Картинка уже была сильно сжата, а голос с фоновой музыкой занимали почти столько же, сколько само видео.

Для этой записи приемлемый результат дали AAC на 80 кбит/с и Opus на 64 кбит/с. Оба варианта кодировал из исходного экспорта, чтобы не сжимать повторно уже пережатую картинку.

H.264 + AAC:

ffmpeg -i source.mp4 -vf scale=1280:720:flags=lanczos 
  -c:v libx264 -crf 28 -preset veryslow 
  -profile:v high -pix_fmt yuv420p 
  -movflags +faststart -c:a aac -b:a 80k out.mp4

VP9 + Opus:

ffmpeg -i source.mp4 -vf scale=1280:720:flags=lanczos 
  -c:v libvpx-vp9 -crf 42 -b:v 0 -row-mt 1 -cpu-used 2 
  -pix_fmt yuv420p -c:a libopus -b:a 64k out.webm

Вариант

Размер, байт

Меньше исходного сжатого варианта

После онлайн-инструментов

2 538 323

-

H.264 / AAC

1 899 883

25%

VP9 / Opus

1 820 663

28%

При подборе параметров главный критерий для скринкаста - читаемость мелкого текста интерфейса.

На странице первым источником указан WebM, запасным - MP4. Параметр +faststart переносит метаданные MP4 в начало файла, чтобы для старта видео не требовалось сначала дожидаться загрузки.

Ролик стал весить около 1,8 МБ. Но скорость первого экрана определяется не только размером ролика.

LCP: сначала постер, потом запуск видео

Следующий этап - замеры в мобильном профиле Lighthouse [8]. Это инструмент аудита Google, доступный в Chrome DevTools. Итоговая оценка показывает общее состояние страницы, а отдельные метрики и данные о загрузке помогают найти причину задержки.

Здесь интересовал Largest Contentful Paint, LCP [9] - время появления крупнейшего подходящего изображения, текстового блока или видео в видимой области. Это не время загрузки всех ресурсов страницы. У видео для замера может использоваться постер или первый отображённый кадр.

В исходном мобильном прогоне LCP составил 4,9 секунды. Элементом LCP был плеер первого экрана.

Сжал постер: 252 КБ → 16 КБ

Статичный кадр перед видео хранился в PNG и весил 252 КБ. Конвертация в WebP сократила его до 16 КБ. Для изображения первого экрана также добавил приоритетную предзагрузку:

<link rel="preload" as="image" fetchpriority="high"
      href="/media/overview-poster-2.webp">

Следующий мобильный замер показал 3,7 секунды. Стало лучше, но оставалось проверить, что происходит при старте воспроизведения.

Почему preload="metadata" не спасает от автоплея

У видео изначально стоял preload="metadata". При этом плеер начинал воспроизведение, как только попадал в видимую область. Для первого экрана это означало запуск сразу после открытия страницы.

preload - подсказка о предварительной загрузке. Когда начинается воспроизведение, браузеру нужны сами медиаданные. Приоритет автоплея над preload отдельно отмечен в MDN [10].

В одном из замеров браузер получил целиком предыдущую сжатую версию ролика - около 2,5 МБ. Уменьшить файл и выбрать момент его загрузки оказалось двумя отдельными задачами.

Сначала перенёс запуск на завершение загрузки страницы. Один из мобильных прогонов дал 2,7 секунды LCP, но ролик по-прежнему начинал скачиваться без участия посетителя.

В итоговом варианте сделали, чтобы воспроизведение начиналось только после первого движения указателя, касания, скролла или нажатия клавиши. Кнопки глав и звука могут запустить его напрямую. Вот сокращённый код:

let armed = false;

function arm() {
  if (armed) return;
  armed = true;
  if (inView && !v.dataset.upaused) {
    v.play().catch(() => {});
  }
}

['pointerdown', 'pointermove', 'touchstart', 'wheel', 'keydown', 'scroll']
  .forEach(event => {
    window.addEventListener(event, arm, { once: true, passive: true });
  });

inView обновляет IntersectionObserver, а data-upaused обозначает паузу, которую поставил пользователь. При уходе плеера за пределы экрана observer останавливает видео. При возвращении - продолжает, если воспроизведение уже разрешено и пользователь не поставил его на паузу.

До взаимодействия доступны постер и навигация по главам. Браузер может загружать метаданные, но команды воспроизводить ещё нет. В таком состоянии первоначальный трафик страницы составил около 400 КБ!

Получилось хорошо - это не режим "скачивать только по кнопке Play", достаточном простого движения мыши. Активный посетитель может почти сразу запустить загрузку оставшихся медиаданных.

Ссылки на главы и разметка для поиска

Главы-тайм-кода ролика можно описать для Google через schema.org [11]: VideoObject с вложенными Clip. Поисковик может использовать их для показа ключевых моментов видео в выдаче.

Один элемент массива hasPart:

{
  "@type": "Clip",
  "name": "Capture & Translate",
  "startOffset": 30,
  "endOffset": 43,
  "url": "https://klipto.me/?t=30"
}

Это только фрагмент. Родительскому VideoObject также нужны обязательные поля, включая name, thumbnailUrl и uploadDate.

По требованиям Google ссылка на главу должна вести на тот же путь страницы видео с дополнительным query-параметром времени. Здесь это ?t=30. Сам плеер должен прочитать параметр, дождаться метаданных и установить currentTime: JSON-LD перемотку не реализует.

Проверять нужно именно ссылку в новой вкладке. Работающая кнопка главы ещё не означает, что переход из поиска откроет нужную секунду.

Понятно дело, что разметка не гарантирует появления глав в выдаче. Остаются требования Google к видео и странице, в том числе к просмотру видео как её основному назначению. Требования и инструменты проверки есть в документации Google [12].

Ещё 118 КБ нашлись в favicon

После переноса видео за пределы начальной загрузки среди оставшихся ресурсов оказалось, что у меня favicon PNG 512×512 размером 121 КБ. Для иконки во вкладке браузера это заметная доля трафика. После оптимизации осталось 2,7 КБ.

Заодно были сжаты остальные изображения страницы. Итоговые числа ниже учитывают и эти изменения, не только работу с видео.

Результаты

Замеры выполнены 21 сентября 2026 года в мобильном профиле Lighthouse с ограничением ресурсов:

Метрика

До

После

Performance score

81

95

LCP

4,9 с

Около 2,9 с

Первоначальный трафик

Около 3,1 МБ

Около 400 КБ

Данных о росте досматриваемости или продаж у меня пока нет. Замеренный результат - более лёгкая начальная загрузка и меньший файл видео. UX-результат, который можно проверить руками, - основные сценарии видны до просмотра, каждый доступен напрямую, а звук включается с начала связного фрагмента.

Автор: akors

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ux/459491

Ссылки в тексте:

[1] айтрекинговом исследовании NN/g 2018 года: https://www.nngroup.com/articles/scrolling-and-attention/

[2] данным Wistia за 2026 год: https://wistia.com/blog/optimal-video-length

[3] chunking, разбиении на смысловые части, которое описывает Nielsen Norman Group: https://www.nngroup.com/articles/chunking/

[4] Astro: https://astro.build/

[5] Cloudflare Pages: https://pages.cloudflare.com/

[6] ezgif: https://ezgif.com/resize-video

[7] mp4compress: https://www.mp4compress.com/

[8] Lighthouse: https://developer.chrome.com/docs/lighthouse/overview

[9] Largest Contentful Paint, LCP: https://web.dev/articles/lcp

[10] MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/video#preload

[11] schema.org: http://schema.org

[12] документации Google: https://developers.google.com/search/docs/appearance/structured-data/video

[13] Источник: https://habr.com/ru/articles/1090594/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1090594