ESPNCampbell, players back DC Sheppard after Lions' defensive woes continueESPN DeportesCheco encabeza críticas a F1 por fallas en MalasiaPunchMotor park killing: Why we handed over operatives to police – Osun AmotekunThe Jerusalem PostIran executes two men arrested over January 2026 protests, Mizan reportsRTP DesportoTrês portugueses em destaque no Rali de MarrocosInquirerHighlights: Day 33 of Sara Duterte impeachment trial | Oct. 5, 2026ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioThe RegisterThe question you would ask HPE and NVIDIA if nobody was recordingBillboardJay Wheeler Announces La Voz Favorita World Tour: All the DatesPopular ScienceExtinct ice age bovine discovered in Siberian caveVarietyJay-Z, Megan Thee Stallion, Keke Palmer, Gina Prince-Bythewood and NY Knicks Champs Named to Ebony Power 100 List (EXCLUSIVE)Deadline‘Things We Never Got Over’ Prime Video Series Adaptation Adds 6 To Cast
The Daily Newsstand · Free, Always
Monday, October 5, 2026

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

Translate

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Сайт собран на Astro и размещён на Cloudflare Pages. Тег <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 для изменения размера и mp4compress для дополнительного сжатия. На выходе получался файл 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. Это инструмент аудита Google, доступный в Chrome DevTools. Итоговая оценка показывает общее состояние страницы, а отдельные метрики и данные о загрузке помогают найти причину задержки.

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

В исходном мобильном прогоне 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.

В одном из замеров браузер получил целиком предыдущую сжатую версию ролика - около 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: 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.

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.