Lazy load video and audio
Media elements (<video> and <audio>) and their associated resources (such as <video poster> images and preloaded metadata) consume significant network and storage bandwidth during initial page load. Even when preload="metadata" or preload="none" is set on a <video>, the browser eagerly fetches its poster image immediately, and preload="metadata" still initiates range requests for media headers across every player on the page.
Adding loading="lazy" to offscreen <video> and <audio> elements instructs the browser to defer all media network requests—including poster image downloads, preload hints, and autoplay execution—until the element reaches a browser-calculated distance from the visual viewport.
How to implement
Section titled “How to implement”- Add
loading="lazy"to offscreen media: Applyloading="lazy"to<video>and<audio>elements positioned below the fold. Omit theloadingattribute on above-the-fold media so it defaults toeager. - Set explicit
widthandheighton<video>elements: Always specify integerwidthandheightattributes (in CSS pixels) on<video>. Unloaded videos default to300×150dimensions; without explicit sizing, a lazy-loaded video may fail to intersect the visible viewport and never load, or cause disruptive layout shifts (CLS) when it loads. - Include the
controlsattribute on<audio>elements: Always includecontrolson<audio loading="lazy">. Because<audio>elements have no intrinsic visual box without browser controls, an<audio loading="lazy">element withoutcontrolscannot visibly intersect the viewport and will never load or autoplay. - Coordinate
preloadandposterattributes:- On
<video loading="lazy">, both theposterimage fetch and thepreloadbehavior (none,metadata, orauto) are deferred until the video approaches the viewport. - Pair
loading="lazy"withpreload="none"when the user must explicitly press play after scrolling to the media, orpreload="metadata"when duration and initial frame data should populate automatically once the player enters the viewport.
- On
- Handle lazy
autoplaysafely: Whenloading="lazy"is combined withautoplayon a muted<video>(or an opt-in<audio controls>), playback and downloading wait until the element approaches the viewport. Always respect@media (prefers-reduced-motion: reduce)before autoplaying motion media.
Example code
Section titled “Example code”<!-- Above-the-fold hero video: DO NOT use loading="lazy". Omit loading (defaults to "eager") so the poster and initial media load immediately. --><video controls width="1280" height="720" poster="/images/hero-poster.jpg" preload="metadata"> <source src="/media/keynote.webm" type="video/webm"> <source src="/media/keynote.mp4" type="video/mp4"> <track src="/media/keynote-en.vtt" kind="captions" srclang="en" label="English"></video>
<!-- Below-the-fold video player: MANDATORY: Specify integer width and height attributes so the unloaded video has a non-zero layout box to intersect the viewport and avoids layout shift. Both the poster image and preload="metadata" are deferred until near the viewport. --><video controls loading="lazy" width="800" height="450" poster="/images/tutorial-poster.jpg" preload="metadata"> <source src="/media/tutorial.webm" type="video/webm"> <source src="/media/tutorial.mp4" type="video/mp4"> <track src="/media/tutorial-en.vtt" kind="captions" srclang="en" label="English"></video>
<!-- Below-the-fold inline product loop: With loading="lazy" and autoplay, downloading and playback wait until the video approaches the viewport. Include muted and playsinline for autoplay. --><video controls loading="lazy" autoplay loop muted playsinline width="640" height="360" poster="/images/feature-loop-poster.jpg"> <source src="/media/feature-loop.webm" type="video/webm"> <source src="/media/feature-loop.mp4" type="video/mp4"> <track src="/media/feature-loop-en.vtt" kind="captions" srclang="en" label="English"></video>
<!-- Below-the-fold audio player: MANDATORY: Include the controls attribute. Lazy-loaded <audio> without controls is invisible, cannot intersect the viewport, and will never load. --><figure> <figcaption id="episode-title">Episode 42: Browser Architecture Deep Dive</figcaption> <audio controls loading="lazy" preload="metadata" src="/media/episode-42.mp3" aria-labelledby="episode-title" aria-details="episode-transcript" ></audio> <details id="episode-transcript"> <summary>View transcript</summary> <p>Transcript content for Episode 42...</p> </details></figure>// Pause autoplaying videos when the user prefers reduced motionconst reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
function syncReducedMotion(prefersReducedMotion) { for (const video of document.querySelectorAll('video[autoplay]')) { if (prefersReducedMotion) { video.removeAttribute('autoplay'); video.pause(); } }}
syncReducedMotion(reducedMotionQuery.matches);reducedMotionQuery.addEventListener('change', (event) => { syncReducedMotion(event.matches);});Best practices
Section titled “Best practices”- MANDATORY: Always set explicit
widthandheightattributes on<video>elements. Unloaded videos have a300×150intrinsic size and will never load if they do not intersect a visible part of the page. - MANDATORY: Always include the
controlsattribute on<audio loading="lazy">elements. Lazy-loaded<audio>elements withoutcontrolsare not rendered visibly and will never load or autoplay. - DO NOT apply
loading="lazy"to above-the-fold or Largest Contentful Paint (LCP)<video>elements, as it delays fetching both theposterimage and video data. - DO omit the
loadingattribute when default eager loading is desired rather than explicitly writingloading="eager". - DO NOT rely on the
windowloadevent to detect when lazy-loaded<video>or<audio>elements have loaded. Theloadevent fires based only on eager-loaded media, even if aloading="lazy"media element is initially inside the visual viewport. - DO note that browsers only defer
loading="lazy"media when JavaScript is enabled (an anti-tracking measure to prevent servers from inferring scroll position via media requests when scripting is disabled).
Fallback strategy
Section titled “Fallback strategy”Baseline: loading-lazy-media
If your Baseline target does not support loading="lazy" on <video> and <audio>, choose between progressive enhancement and an IntersectionObserver fallback based on how strictly bandwidth must be conserved:
Progressive enhancement (recommended default)
Section titled “Progressive enhancement (recommended default)”Browsers that do not recognize loading="lazy" on <video> and <audio> ignore the attribute and fall back to standard loading governed by the preload attribute. Pairing loading="lazy" with preload="none" or preload="metadata" ensures unsupported browsers still avoid downloading the full media file upfront, though <video poster> images and preload="metadata" headers will be fetched eagerly.
Custom code fallback (IntersectionObserver)
Section titled “Custom code fallback (IntersectionObserver)”When your Baseline target requires strictly deferring <video poster> downloads, preload="metadata" requests, or viewport-triggered playback in browsers without native support, feature-detect Object.hasOwn(HTMLMediaElement.prototype, 'loading'), then fall back to IntersectionObserver:
// Feature-detect native media lazy loading on HTMLMediaElement.prototypeconst supportsLazyMedia = Object.hasOwn(HTMLMediaElement.prototype, 'loading');
if (!supportsLazyMedia) { const lazyMediaElements = document.querySelectorAll( 'video[loading="lazy"], audio[loading="lazy"][controls]', );
// Example-only rootMargin value; adjust distance threshold for your layout const mediaObserver = new IntersectionObserver( (entries, observer) => { for (const entry of entries) { if (!entry.isIntersecting) continue;
const media = entry.target;
// Restore deferred poster and preload attributes when entering the viewport if (media.dataset.poster) { media.poster = media.dataset.poster; delete media.dataset.poster; }
if (media.dataset.preload) { media.preload = media.dataset.preload; delete media.dataset.preload; }
if (media.dataset.src) { media.src = media.dataset.src; delete media.dataset.src; }
for (const source of media.querySelectorAll('source[data-src]')) { source.src = source.dataset.src; delete source.dataset.src; }
media.load(); observer.unobserve(media); } }, { rootMargin: '200px 0px' }, );
for (const media of lazyMediaElements) { // Hold back network fetches in unsupported browsers until intersection if (media.hasAttribute('poster')) { media.dataset.poster = media.getAttribute('poster'); media.removeAttribute('poster'); } if (media.hasAttribute('preload') && media.getAttribute('preload') !== 'none') { media.dataset.preload = media.getAttribute('preload'); } media.preload = 'none'; mediaObserver.observe(media); }}Compatibility notes: Badges like Baseline: … mark web features referenced by this guide. Baseline status describes availability across the core browser set — it does not mean every browser or older version supports the feature. Feature data as published upstream, synced 2026-11-12.
Live demo
Section titled “Live demo”Open the demo standalone · Demo source on GitHub
Source: GoogleChrome/modern-web-guidance-src (Apache-2.0), upstream commit 179dbc6, synced 2026-11-12. Local changes: macros expanded to links/badges, first heading de-duplicated, demo and this footer added. This is an independent, unofficial site — not a Google publication.