- PVSM.RU - https://www.pvsm.ru -
Для главной страницы я сделал минутное видео с четырьмя главами: можно посмотреть всё демо или сразу перейти к интересующей функции. Плеер работает на обычном HTML <video> и небольшом JS-контроллере, без сторонних библиотек.
Сам ролик удалось сжать с 38 до 1,8 МБ. Но для быстрой загрузки страницы этого оказалось недостаточно: пришлось уменьшить картинку-заставку и отложить запуск видео. В итоге при открытии страница загружает около 400 КБ вместо 3,1 МБ, а видео - уже после первого действия посетителя.
В статье покажу код плеера, команды FFmpeg и результаты замеров. Объясню, почему preload="metadata" не спасает при автоплее, зачем возвращать видео к началу главы при включении звука и как добавить разметку таймкодов для Google.
Я Антон, разрабатываю менеджер буфера обмена для macOS Klipto.
На главной сайта нужно было показать четыре сценария/фичи: вставку скопированных значений в поля гугл таблицы (функция Stack), поиск по истории буфера, захват текста с экрана с переводом и работу с заметками.
Если показать их последовательно в ролике, то лишь небольшая часть досмотрит ролик и узнает обо всех фичах. Если разнести по странице - придётся рассчитывать, что люди будут скроллить.
При выборе компоновки я опирался на два исследования:
В айтрекинговом исследовании NN/g 2018 года [1] на первый экран приходилось 57% времени просмотра страницы, на первые два - 74%
По данным Wistia за 2026 год [2], средний engagement роликов короче минуты составляет 52%. Это средняя просмотренная доля видео, а не процент людей, досмотревших до конца
То есть рассчитывать, что человек обязательно проскроллит страницу или посмотрит всё демо, оснований мало.
Вместо кнопки "Посмотреть демо" с попапом как обычно делают на продуктовых сайтах я показываю видео прямо на первом экране. Под ним четыре карточки: название сценария/фичи, короткое описание и прогресс-бар. Каждая карточка работает как таймкод. Заинтересовала фича - нажимаешь на третью и переходишь к нужному фрагменту.

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 & 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 МБ. Но скорость первого экрана определяется не только размером ролика.
Следующий этап - замеры в мобильном профиле Lighthouse [8]. Это инструмент аудита Google, доступный в Chrome DevTools. Итоговая оценка показывает общее состояние страницы, а отдельные метрики и данные о загрузке помогают найти причину задержки.
Здесь интересовал Largest Contentful Paint, LCP [9] - время появления крупнейшего подходящего изображения, текстового блока или видео в видимой области. Это не время загрузки всех ресурсов страницы. У видео для замера может использоваться постер или первый отображённый кадр.
В исходном мобильном прогоне LCP составил 4,9 секунды. Элементом LCP был плеер первого экрана.
Статичный кадр перед видео хранился в PNG и весил 252 КБ. Конвертация в WebP сократила его до 16 КБ. Для изображения первого экрана также добавил приоритетную предзагрузку:
<link rel="preload" as="image" fetchpriority="high"
href="/media/overview-poster-2.webp">
Следующий мобильный замер показал 3,7 секунды. Стало лучше, но оставалось проверить, что происходит при старте воспроизведения.
У видео изначально стоял 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].
После переноса видео за пределы начальной загрузки среди оставшихся ресурсов оказалось, что у меня 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
Нажмите здесь для печати.