Video is the most bandwidth-intensive content you'll serve. A single unoptimized 1080p video can be 200MB — more than an entire page's worth of images, scripts, and stylesheets combined. Getting video right means choosing the correct codec, container, compression settings, and loading strategy.
This guide covers the practical decisions: which format to encode, how to embed with fallbacks, how to control loading behavior, and when to reach for adaptive streaming over progressive download.
The HTML5
The <video> element is the foundation. At minimum:
<video controls width="1280" height="720" preload="metadata" poster="thumb.jpg">
<source src="video.webm" type="video/webm; codecs=vp9,opus">
<source src="video.mp4" type="video/mp4">
<p>Your browser doesn't support HTML5 video. <a href="video.mp4">Download the video</a>.</p>
</video>Key attributes:
controls— shows play/pause, seek bar, volume, fullscreen. Always include for user-initiated video.width/height— prevents CLS by reserving space before the video loadspreload— controls how much data the browser fetches before play:none(nothing),metadata(duration, dimensions),auto(browser decides, may buffer the whole thing)poster— thumbnail image shown before playback. Critical for perceived performance.
Formats and Codecs: What Actually Matters
A video file has two layers: the container (MP4, WebM, MKV) and the codecs inside it (H.264, VP9, AV1 for video; AAC, Opus, MP3 for audio). The container is the packaging; the codec determines file size and quality.
| Codec | Container | Browser Support | Compression | Use Case |
|---|---|---|---|---|
| H.264 (AVC) | MP4 | 100% | Good | Universal fallback, mobile |
| H.265 (HEVC) | MP4 | Safari, some Edge | Better (30-50% vs H.264) | Apple ecosystem only |
| VP9 | WebM | Chrome, Firefox, Edge, Safari 14.1+ | Better (30-50% vs H.264) | YouTube's primary codec |
| AV1 | WebM or MP4 | Chrome, Firefox, Safari 17+ | Best (30% vs VP9) | Next-gen, still encoding-intensive |
The practical recommendation: Encode two versions: WebM/VP9 (smaller, modern browsers) and MP4/H.264 (universal fallback). Use <source> elements to serve WebM first, MP4 as fallback.
Codec Strings in the type Attribute
The type attribute on <source> lets browsers skip formats they can't decode without downloading them. Include the codec string for more precise matching:
<!-- H.264 Baseline Profile Level 3.0, AAC-LC -->
<source src="video.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
<!-- H.264 High Profile Level 4.0 -->
<source src="video-hd.mp4" type='video/mp4; codecs="avc1.64001E, mp4a.40.2"'>
<!-- VP9 + Opus -->
<source src="video.webm" type='video/webm; codecs="vp9, opus"'>
<!-- AV1 + Opus -->
<source src="video-av1.mp4" type='video/mp4; codecs="av01.0.04M.08, opus"'>Why codec strings matter: Without them, the browser might start downloading an MP4 only to discover it uses HEVC (which it can't decode in Chrome), wasting bandwidth. The codec string prevents this by letting the browser reject incompatible files before any download.
Preload Strategies
The preload attribute has three values with dramatically different bandwidth implications:
preload="none" — Downloads nothing until the user clicks play. Best for pages with many videos or videos below the fold. The poster image displays immediately (it's a separate request from the video).
preload="metadata" — Downloads just enough to determine duration, dimensions, and potentially a few frames. Typically 50-500KB. This is the recommended default for most use cases — the browser can show the duration in the controls and the video is ready to play with minimal delay.
preload="auto" — Lets the browser decide how much to buffer. On desktop with a fast connection, this may buffer the entire video. On mobile or with data saver enabled, it behaves like metadata. Use this only for the primary video on a page where instant playback matters.
<!-- Main content video: preload metadata for quick start -->
<video preload="metadata" poster="main-thumb.jpg" controls>...</video>
<!-- Gallery of 20 video thumbnails: preload nothing -->
<video preload="none" poster="gallery-thumb.jpg" controls>...</video>
<!-- Hero background video: preload for instant playback -->
<video preload="auto" autoplay muted loop playsinline>...</video>
Autoplay Rules: When Browsers Block It
Browsers restrict autoplay to prevent pages from blasting audio at users. The rules (as of 2026):
- Muted autoplay: Always allowed.
<video autoplay muted>plays immediately in all browsers. - Unmuted autoplay: Blocked by default. Allowed only if the user has previously interacted with the site (Chrome's MEI score), the site is whitelisted, or the user has tapped play before.
<!-- This always works (muted) -->
<video autoplay muted loop playsinline poster="bg.jpg">
<source src="bg.webm" type="video/webm">
<source src="bg.mp4" type="video/mp4">
</video>
<!-- This gets blocked on most first visits (unmuted) -->
<video autoplay>
<source src="intro.mp4" type="video/mp4">
</video>The playsinline attribute is required for autoplay on iOS Safari. Without it, the video opens in fullscreen on iPhones. Always include it for background/decorative videos.
Handling blocked autoplay gracefully:
const video = document.querySelector('video');
video.play().catch(() => {
// Autoplay was blocked — show a play button overlay
document.querySelector('.play-overlay').style.display = 'flex';
});
Video Compression for Web Delivery
Raw screen recordings and camera footage are massively oversized for web delivery. Use FFmpeg (the industry standard) to compress:
# H.264 MP4 — universal compatibility
ffmpeg -i input.mov -c:v libx264 -crf 23 -preset slow \
-c:a aac -b:a 128k -movflags +faststart output.mp4
# VP9 WebM — 30-50% smaller than H.264
ffmpeg -i input.mov -c:v libvpx-vp9 -crf 30 -b:v 0 \
-c:a libopus -b:a 96k output.webm
# AV1 (slow encoding, best compression)
ffmpeg -i input.mov -c:v libaom-av1 -crf 30 -cpu-used 4 \
-c:a libopus -b:a 96k output.mp4Key flags explained:
-crf— Constant Rate Factor. Lower = better quality, larger file. H.264: 18-28 is sane range, 23 is default. VP9: 25-35.-preset slow— Spend more time encoding for smaller files.slowis a good balance.veryslowsaves 3-5% more but takes 4x longer.-movflags +faststart— Moves the MP4 metadata to the beginning so the browser can start playback before downloading the entire file. Always include this for web MP4s.-b:v 0— With VP9/AV1, enables pure quality-based encoding (CRF mode) without a bitrate cap.
Target Bitrates by Resolution
| Resolution | H.264 Bitrate | VP9 Bitrate | Use Case |
|---|---|---|---|
| 480p | 1-2 Mbps | 0.7-1.4 Mbps | Mobile, low bandwidth |
| 720p | 2.5-5 Mbps | 1.8-3.5 Mbps | Standard web video |
| 1080p | 5-10 Mbps | 3.5-7 Mbps | High quality, desktop |
| 4K | 15-30 Mbps | 10-20 Mbps | Premium content |
For CRF-based encoding, these bitrates are approximate — CRF adjusts dynamically per scene. A static talking head at CRF 23 might produce 2 Mbps; a fast-action scene at the same CRF might produce 8 Mbps.
Adaptive Streaming: HLS and DASH
For video longer than 2-3 minutes, progressive download (single file) has problems: the entire file must be a single resolution, and seeking to the middle requires downloading everything before it (unless Range requests work, which they usually do on CDNs but not always on origin servers).
Adaptive streaming solves both: the video is split into 2-10 second segments at multiple quality levels. The player monitors bandwidth and switches quality dynamically.
HLS (HTTP Live Streaming): Apple's protocol. Uses .m3u8 playlists and .ts segments. Supported natively in Safari and via hls.js library in other browsers.
DASH (Dynamic Adaptive Streaming over HTTP): Open standard. Uses .mpd manifests and .m4s segments. Supported via dash.js or Shaka Player.
# Generate HLS with FFmpeg (3 quality levels)
ffmpeg -i input.mp4 \
-filter_complex "[0:v]split=3[v1][v2][v3];\
[v1]scale=w=1280:h=720[v1out];\
[v2]scale=w=854:h=480[v2out];\
[v3]scale=w=640:h=360[v3out]" \
-map "[v1out]" -c:v:0 libx264 -b:v:0 3000k -maxrate:v:0 3500k -bufsize:v:0 6000k \
-map "[v2out]" -c:v:1 libx264 -b:v:1 1500k -maxrate:v:1 1800k -bufsize:v:1 3000k \
-map "[v3out]" -c:v:2 libx264 -b:v:2 800k -maxrate:v:2 1000k -bufsize:v:2 1600k \
-map a:0 -c:a aac -b:a 128k \
-f hls -hls_time 4 -hls_playlist_type vod \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" \
stream_%v/playlist.m3u8Playing HLS in browsers:
import Hls from 'hls.js';
const video = document.querySelector('video');
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari: native HLS
video.src = 'master.m3u8';
} else if (Hls.isSupported()) {
// Other browsers: hls.js
const hls = new Hls();
hls.loadSource('master.m3u8');
hls.attachMedia(video);
}
Replacing GIFs with Video
Animated GIFs are catastrophically inefficient. A 5-second 480p GIF can easily be 15MB. The same content as MP4 is 200-500KB. Replace GIFs with <video> for identical visual behavior:
<!-- Behaves exactly like a GIF: autoplay, loop, no controls, no audio -->
<video autoplay muted loop playsinline width="480" height="270" poster="preview.jpg">
<source src="animation.webm" type="video/webm">
<source src="animation.mp4" type="video/mp4">
</video>Convert existing GIFs to video for massive savings: GIF to MP4 | GIF to WebM
FFmpeg conversion:
# GIF to MP4 (optimized for web)
ffmpeg -i animation.gif -vf "crop=trunc(iw/2)*2:trunc(ih/2)*2" \
-c:v libx264 -crf 23 -preset slow -pix_fmt yuv420p \
-movflags +faststart -an output.mp4The -pix_fmt yuv420p ensures compatibility with all browsers (some reject yuv444p). The crop filter ensures even dimensions (H.264 requires even width and height).
Poster Images: The Often-Forgotten Optimization
Without a poster attribute, the browser shows either a black rectangle or the first video frame (which requires downloading the video's beginning). A poster image displays instantly and costs almost nothing:
<!-- Generate a poster from the video -->
<!-- ffmpeg -i video.mp4 -ss 00:00:02 -vframes 1 poster.jpg -->
<video poster="poster.webp" preload="none" controls>
<source src="video.mp4" type="video/mp4">
</video>Use preload="none" + poster together: the user sees the poster immediately with zero video bandwidth, and the video only loads when they click play. This is the most bandwidth-efficient pattern for pages with multiple videos.
Web video optimization comes down to three decisions: format (VP9 + H.264 fallback), loading strategy (poster + metadata preload), and compression (CRF-based encoding with FFmpeg). Get these right and you've covered 90% of the performance surface area.
For long-form content, invest in HLS or DASH adaptive streaming to handle varying network conditions. For short clips and GIF replacements, progressive download with proper compression is simpler and perfectly adequate.