How to Lazy-Load Video Without Hurting LCP
This guide is part of Video Performance Optimization within Image & Media Optimization. Unlike images, <video> has no loading="lazy" attribute. Instead, the preload attribute and the presence of src/<source> elements decide what downloads, and browser defaults vary: with preload="auto" (or the default in some browsers), a video below the fold can start downloading megabytes as soon as the HTML is parsed, competing with the LCP image and CSS on the critical path.
Lazy loading video means two things: telling the browser not to fetch video data until needed, and loading or playing videos as they approach the viewport. Done correctly, below-the-fold videos cost nothing during load. Done incorrectly — lazy-loading the hero, or removing the poster — it makes LCP worse.
Rapid Diagnosis
- Filter the Network panel by Media and reload: any video requests before LCP on templates where video is below the fold are wasted critical-path bytes.
- Check
preloadattributes on every<video>(and defaults set by video components). - Check posters. Videos without posters show empty boxes or rely on the first frame.
- Check dimensions. Videos and their containers need
width/heightoraspect-ratioto avoid layout shift when metadata loads.
Root Cause Analysis
1. Default or auto preload below the fold. The browser treats the video as likely to play and fetches data early.
2. Autoplay loops with src set at load. Autoplay videos must download to play, so every autoplay loop on the page downloads immediately.
3. Lazy-loading the hero. Deferring the above-the-fold video delays its poster or first frame, harming LCP.
4. No reserved space. Without dimensions, the video element's size is unknown until metadata loads, causing a shift.
Step-by-Step Resolution
1. Keep the hero eager, with a preloaded poster
<video autoplay muted loop playsinline width="1280" height="720"
poster="/media/hero-poster-1280.avif" preload="metadata">
<source src="/media/hero-720.webm" type="video/webm">
<source src="/media/hero-720.mp4" type="video/mp4">
</video>
<link rel="preload" as="image" href="/media/hero-poster-1280.avif" fetchpriority="high">
<!-- trade-off: preload="metadata" on an autoplay video still lets it start
promptly once the page is ready; "auto" would compete with the poster. -->
2. Use preload="none" for click-to-play video below the fold
<video controls preload="none" width="1280" height="720" poster="/media/demo-poster.avif">
<source src="/media/demo.mp4" type="video/mp4">
</video>
<!-- trade-off: playback starts a little later after the user presses play,
because nothing was buffered. For short demos users nearly always watch,
preload="metadata" is a reasonable compromise. -->
3. Load autoplay loops when they approach the viewport
const io = new IntersectionObserver((entries) => {
for (const { target: video, isIntersecting } of entries) {
if (isIntersecting) {
for (const s of video.querySelectorAll('source[data-src]')) { s.src = s.dataset.src; s.removeAttribute('data-src'); }
video.load();
video.play().catch(() => {});
} else if (!video.paused) {
video.pause();
}
}
}, { rootMargin: '200px 0px' });
document.querySelectorAll('video[data-lazy]').forEach((v) => io.observe(v));
// trade-off: on very fast scrolling the video may still be loading when it
// enters view; the poster covers the gap, so always set one.
Expected outcome: zero video bytes during load for below-the-fold loops; videos start just before they become visible.
4. Reserve space for every video
Set width and height attributes (or aspect-ratio in CSS) on every video and embed container so the layout never depends on metadata arriving.
Verification
On a throttled profile, check that no video requests start before LCP on pages where video is below the fold, and that the hero's poster loads early with high priority. Scroll to lazy videos and confirm they load and play shortly before entering view. Layout Shift Regions should not flash when videos load.
Worked Example: A Recipe Site
A recipe site embedded three short cooking loops per article, all autoplay muted loop with sources set at load. Each article downloaded about 4MB of video before LCP on mobile, and LCP p75 was 3.4s. Switching the loops to visibility-triggered loading with posters, and setting preload="none" on the full-length recipe video, removed all video bytes before LCP; LCP p75 dropped to 2.2s and mobile data usage per article view fell by 60%, since many readers never scrolled to the second and third loops.
Lazy Loading in Frameworks and Components
Many video components set preload="auto" or attach sources eagerly for convenience. Audit them once: a shared <LazyVideo> component that accepts priority (eager hero) or default (lazy) and handles posters, dimensions and the observer centralises the policy so individual pages cannot regress it. In React and Vue, attach the IntersectionObserver in an effect and clean it up on unmount; in SPAs, also pause and unload videos when their route is left, so a video from the previous view does not keep downloading.
Measuring Video Loading in the Field
Add two signals to RUM for pages with video: bytes of media downloaded before LCP (from Resource Timing entries whose initiatorType is video or whose URLs match your media paths), and time to first frame for videos that play (from the playing event relative to when the video entered the viewport). The first confirms that lazy loading keeps video off the critical path for real users; the second confirms you have not overcorrected — if videos routinely start playing a second after becoming visible, widen the observer's margin or switch to preload="metadata" for that template.
Common Mistakes
- Removing the poster to "save bytes". The poster is what users see first and often the LCP element.
- Lazy-loading the hero. LCP waits for the observer and the download.
- Forgetting
playsinline. On iOS, autoplaying videos without it may open fullscreen or not play. - Leaving videos playing offscreen. They keep consuming bandwidth and CPU.
Edge Cases
Safari and preload. Safari has historically treated preload as a hint with its own defaults (often metadata); test actual behaviour.
loading="lazy" on iframes. Video embeds in iframes can use native lazy loading, but a facade saves more by not creating the iframe at all.
Low Power Mode. iOS may block autoplay; lazy-loading logic must handle a rejected play() promise gracefully.
Background tabs. Browsers pause video in background tabs; there is no need to manage that yourself.
FAQ
Is there a native loading="lazy" for video?
Not for <video> at the time of writing. Use preload="none" and set sources on visibility. Iframes (for embeds) support loading="lazy".
Does preload="none" prevent the poster from loading?
No. The poster is fetched as an image regardless of preload. Only video data is deferred.
What rootMargin should I use?
One to two viewport heights on mobile (where scrolling is fast) and less on desktop. The goal is for the first frames to be ready when the video becomes visible.
Should autoplay loops ever be eager?
Only when they are above the fold and essential to the first impression. Even then, a poster should paint first and serve as the LCP element.
Does lazy-loaded video affect SEO?
Search engines discover videos through markup and structured data, not playback. Keep sources discoverable (in data-src or via a VideoObject schema) and provide posters and descriptions.
How do I measure the effect?
Track bytes downloaded before LCP (from Resource Timing filtered to media) and LCP p75 on video templates before and after. Add a time-to-first-frame metric for lazy videos to confirm playback still starts promptly.
What about video in modals and lightboxes?
Do not create the video element (or set its sources) until the modal opens. Preload the poster on hover of the trigger if the modal opens with a visible frame, and prefetch the first video segment or file only when the user signals intent.
Related
- Video poster images and LCP — making the poster fast.
- Native lazy loading vs IntersectionObserver — the same trade-offs for images.
- Lazy-loading iframes — for embedded players.