There is a category of site where the product is motion. A DJ, a venue, a production studio, a gym, a band. Show a photograph and you are describing the thing. Show three seconds of the room and you are the thing.
Which is why "put a video in the hero" keeps getting asked for, and why it keeps going wrong. The usual result is a page that scores badly, costs mobile visitors real money in data, and makes the strongest asset on the site into the reason it feels slow.
It does not have to work that way. The fix is not a smaller file. It is not a file at all.
Why one MP4 is the problem, not the solution
Drop a single MP4 into the hero and you have made one encode responsible for every visitor. That is an impossible brief:
Encode it for a desktop monitor and a mid-range Android on train LTE waits, then waits some more, then sees something.
Encode it for the phone and the footage looks thin on the screens where it had the best chance of landing.
Either way the browser has to buffer a meaningful chunk before playback feels safe to start, and it has to pull that chunk from the top of the file, at full quality, whether or not the connection can sustain it.
There is also the part nobody sees in a Lighthouse run: a visitor on a metered plan who bounces after four seconds still paid for however much of that file the browser had already fetched.
Every knob you can turn on a single file, resolution, bitrate, duration, is a trade between two groups of visitors. Adaptive streaming removes the trade instead of resolving it.
What adaptive delivery actually is
Instead of one file, you produce a ladder: the same footage encoded several times, from a small low-bitrate rendition up to full HD. Each rendition is then cut into short segments, a few seconds each, and a playlist file describes what exists.
For HLS that playlist is an .m3u8. Building the ladder is an FFmpeg job, and it belongs in your build or asset pipeline, not in a request handler. It runs once per clip, not once per visitor.
At playback time the player does something a plain <video> tag cannot: it measures real throughput as segments arrive, and picks the rendition for the next segment accordingly. So the behaviour on a weak connection is not "buffer for six seconds", it is "drop to a lower rung and keep playing".
Three consequences worth being explicit about:
Playback starts on a low rung. The first frame appears fast, then quality climbs as the player learns what the connection can sustain. A viewer notices a slow start far more than they notice the first two seconds being softer.
It goes down as well as up. This is the part that matters on real mobile networks, where bandwidth is not merely low, it is uneven. A player that only climbs is a player that stalls the first time somebody walks into a lift.
Only watched segments are fetched. Somebody who scrolls past after three seconds has downloaded three seconds. Not the header of a 1080p master.
The browser split you have to handle
Safari and iOS play HLS natively. Point a <video> element at the .m3u8 and it works, with the platform's own adaptive logic.
Everything else needs a JavaScript player, HLS.js being the standard choice, which uses Media Source Extensions to feed segments to the video element itself.
So the correct implementation is: feature detect native HLS support first, use it when it is there, and only load the player library when it is not. Loading the library unconditionally means shipping a payload to iOS users that their browser was never going to need, on the exact platform where you were trying to save bytes.
The poster is doing more work than you think
The stream solves playback. It does not solve the first paint, because even a fast first segment is a network round trip away.
So the hero needs a poster image, and that poster is almost certainly your Largest Contentful Paint element. Treat it as one:
Serve it as AVIF or WebP with a JPEG fallback, sized for the viewport rather than sized once for the largest screen you support.
Do not lazy-load it. It is above the fold, it is the LCP candidate, and lazy loading it is the single most common way teams accidentally push their own LCP out by a second.
Keep the video payload lazy instead. The poster paints, the stream starts, and the handover happens when the first segment is decodable.
Match the poster to the first frame. If they differ visibly, the handover reads as a glitch rather than as playback beginning.
Get this ordering right and the video stops competing with your Core Web Vitals, because the thing being measured is an image you fully control.
Respect the signals the visitor already sent
Two of these are cheap and routinely skipped:
prefers-reduced-motionis a real accessibility request, not a preference toggle. Honour it by showing the poster and not autoplaying.Save-Data, and the reduced-data preference where it is available, tells you the visitor is on a connection or a plan where your hero is not the priority. Serve the poster, offer play as an explicit choice.
Both cases degrade to a page that still works and still looks like the thing it is selling, which is the whole argument for having a strong poster in the first place.
Where this is worth doing
Not everywhere. A three second loop of an abstract gradient does not need a bitrate ladder, it needs to be a smaller file or an animation.
It is worth it when the footage is the argument: when a visitor deciding whether to book you is deciding on the basis of what the room looks like. In that case the video is not decoration that has to justify its weight. It is the page, and it deserves the delivery pipeline that lets it load like it isn't.
We built exactly this for a DJ's portfolio, where the first screen is real footage from events and most visitors arrive on a phone: