A hero video is usually the largest element in the viewport. That makes it a candidate for Largest Contentful Paint, whether you intended it or not. The metric measures when the biggest piece of content finishes rendering. A video has to be fetched, buffered and decoded before its first frame appears, which is a slow way to satisfy it. The fix is mostly about deciding what paints first, and secondarily about knowing which autoplay rules browsers enforce without telling you.
TL;DR
LCP times when the largest element finishes rendering. A video needs a manifest, a segment and a decode first, which is a slow way to satisfy it. Give the browser a poster to paint instead, straight from images.fastpix.com/{PLAYBACK_ID}/thumbnail.jpg, sized to the viewport. Load the player inside useEffect, which keeps about 228 KB of player script out of the initial bundle on a page whose whole point is painting fast. Add muted alongside auto-play, since browsers block autoplay with sound outright. The attribute is auto-play with a hyphen, not autoplay. Use enable-lazy-loading on below-fold videos only, never on the hero.
Give Next.js LCP a poster image to paint
The browser doesn't wait for your video to be meaningful. It picks the largest contentful element and times when it renders.
A poster image satisfies that measurement immediately. An image starts painting as soon as bytes arrive, not after a manifest, a segment and a decode. The video then arrives behind it and takes over, and LCP was already recorded against the image.
FastPix serves a still frame for any playback ID with no upload and no extra API call:
https://images.fastpix.com/{PLAYBACK_ID}/thumbnail.jpg?time=2&width=1600time picks the frame in seconds from the start. width and height size it, and fit_mode controls cropping. Without time the default frame is the midpoint of the video, not the first frame, so the default is usually a reasonable picture already. Set time when you want a specific moment, and check the result rather than assuming, because a midpoint frame can land on a cut or a transition.
| What paints | Typical cost |
|---|---|
| Poster image, sized to the viewport | One request, decodes immediately |
| Video first frame | Manifest, then a segment, then a decode |
What you need before you start
- A Next.js application on the App Router.
- A FastPix playback ID for a public video.
- A performance trace you can compare against, because "it feels faster" isn't a measurement.
Load the Next.js video player after mount
<fastpix-player> is a web component and registers itself against customElements, which is a browser-only API. Version 1.0.21 and later guard that registration, so a module-scope import no longer throws during server rendering. The reason to defer it is weight: a static import puts roughly 228 KB of player script, hls.js included, into the bundle that blocks your hero.
Adding "use client" doesn't fix it, which is the part that costs people an hour. Client components are still server-rendered for the first response. The import has to be deferred until after mount:
"use client";
import { useEffect } from "react";
export default function HeroVideo({ playbackId }: { playbackId: string }) {
useEffect(() => {
import("@fastpix/fp-player");
}, []);
return (
<fastpix-player
playback-id={playbackId}
stream-type="on-demand"
poster={`https://images.fastpix.com/${playbackId}/thumbnail.jpg?width=1600`}
auto-play
muted
loop
/>
);
}The element upgrades itself as soon as the definition arrives, so nothing is lost by the delay. It also means the player's JavaScript isn't in your initial bundle, which is a second, quieter win. The rest of the attribute surface is in install the FastPix web player.
You can put a real adaptive stream behind this before committing to it. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.
Why a muted attribute is required for Next.js video autoplay
Browsers block autoplay with sound. This isn't configurable and not negotiable, and a hero video that tries it simply doesn't start.
muted is what makes autoplay allowed, so on a background video it isn't a stylistic choice. The player's attribute is auto-play, hyphenated, which is the spelling that trips people who are used to the native <video> element. Pair it with loop and no controls, because a decorative video with a scrubber invites interaction you don't want.
Low Power Mode on iOS blocks autoplay even when muted. A visitor on a phone with a low battery sees the poster and nothing else. That's a good reason for the poster to be a complete composition rather than a placeholder. And prefers-reduced-motion is a real signal from a real person; honouring it by not autoplaying costs you nothing and is the correct default.
@media (prefers-reduced-motion: reduce) {
fastpix-player { display: none; }
}Lazy loading is for background videos below the fold
enable-lazy-loading defers the player's initialisation until the element is near the viewport. It doesn't defer the bundle download, which is what the dynamic import() above already handles. On a hero video it's the wrong tool, because the hero is already in the viewport.
Where it earns its place is on everything else. A page with four product videos further down loads four players on arrival without it. The browser's connection budget goes on media nobody has scrolled to yet.
So the rule is per element rather than per page. Hero video loads. Below-fold videos wait. The general technique for plain HTML video is covered in guide to lazy loading HTML videos for your website.
What adaptive streaming gives a background video
A background video is served like any other video, which means the viewer's connection picks the rendition rather than you picking it.
That's the part a self-hosted MP4 can't do. One file means one bitrate. A phone on a weak connection downloads the same bytes as a desktop on fibre. Either the phone stalls, or the desktop gets a soft-looking video. With HLS the player negotiates, and a hero on a slow connection quietly gets a lower rendition rather than a stall.
The practical consequence for a decorative video is that you can stop hand-tuning a compromise bitrate. Upload the good version and let the ladder handle the rest.
Measure the LCP change, do not assume it
LCP should improve as soon as the poster is the largest painted element, and that shows up in a lab trace immediately. If it doesn't, the video is still winning the "largest" calculation, usually because the poster is smaller than the video's rendered box.
If you're also instrumenting playback, the same deferred import applies. Adding video analytics to a Next.js app covers the metadata split.
Total bytes on first load should fall once the player import moves into useEffect, because the player's JavaScript leaves the initial bundle. Bytes moved shows in the network panel rather than in the vitals. It's also the change most likely to be undone by a future refactor that "tidies up" the dynamic import.
Ship a hero video that does not cost you LCP
Point a sized poster at images.fastpix.com/{PLAYBACK_ID}/thumbnail.jpg, import the player inside useEffect, and set auto-play muted loop with no controls. Then run a trace and confirm LCP is now the image. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Put a real adaptive stream behind your hero and measure the difference.
Frequently Asked Questions (FAQs)
How do I add a background video in Next.js without hurting LCP?
Give the browser a poster image to paint first, sized to the viewport. LCP is then recorded against an image rather than a decoded video frame. FastPix serves one for any playback ID from images.fastpix.com/{PLAYBACK_ID}/thumbnail.jpg. Then load the player inside useEffect so its JavaScript is not in the initial bundle.
Why does my Next.js video autoplay not work?
Almost always because it is not muted. Browsers block autoplay with sound, and that policy is not configurable. Add the muted attribute alongside auto-play. On iOS, Low Power Mode blocks autoplay even when muted, so the poster has to look complete on its own.
Why load the player inside useEffect rather than at module scope?
Because a static import ships the player and hls.js, roughly 228 KB, in the bundle that has to arrive before your hero paints. Older releases also threw ReferenceError: window is not defined during server rendering, but 1.0.21 and later guard the customElements registration, so the crash is no longer the reason. Import it in an effect and the element upgrades itself once the definition lands.
Should I use enable-lazy-loading on a hero video?
No. It defers the player's initialization until the element is near the viewport, and a hero is already in the viewport, so it adds delay for no benefit. It is also not a bundle-size fix; the dynamic import inside useEffect is what keeps the player out of the initial bundle. Use it on videos below the fold, where it stops the browser spending its connection budget on media nobody has scrolled to.
Is a self-hosted MP4 fine for a background video?
It works, and it serves one bitrate to every viewer. A phone on a weak connection downloads the same bytes as a desktop on fiber. One gets a stall, the other gets a soft-looking video. An adaptive stream lets the player negotiate the rendition, which removes the need to hand-pick a compromise bitrate.






