Lazy loading voor video en audio

Media-elementen (<video> en <audio>) en de bijbehorende resources (zoals <video poster>-afbeeldingen en vooraf geladen metadata) verbruiken aanzienlijke netwerk- en opslagbandbreedte tijdens het laden van de pagina. Zelfs als preload="metadata" of preload="none" is ingesteld op een <video>, haalt de browser de poster-afbeelding onmiddellijk op en start preload="metadata" nog steeds bereikverzoeken voor mediakoppen voor elke speler op de pagina.

Als je loading="lazy" toevoegt aan elementen buiten het scherm <video> en <audio>, krijgt de browser de instructie om alle netwerkverzoeken voor media uit te stellen, waaronder poster downloads van afbeeldingen, preload hints en autoplay uitvoering, totdat het element een door de browser berekende afstand tot de visuele viewport bereikt.

Hoe wordt dit geïmplementeerd

  1. Voeg loading="lazy" toe aan media buiten het scherm: Pas loading="lazy" toe op <video>- en <audio>-elementen die onder de vouw zijn geplaatst. Laat het kenmerk loading weg bij media boven de vouw, zodat de standaardwaarde eager wordt gebruikt.
  2. Stel expliciete width en height in voor <video>-elementen: Geef altijd gehele width- en height-kenmerken (in css-pixels) op voor <video>. Niet-geladen video's hebben standaard de afmetingen 300×150. Zonder expliciete afmetingen kan een lazy-loaded video de zichtbare viewport misschien niet kruisen en nooit worden geladen, of storende verschuivingen in de vormgeving (CLS) veroorzaken wanneer deze wordt geladen.
  3. Neem het kenmerk controls op in <audio>-elementen: Neem controls altijd op in <audio loading="lazy">. Omdat <audio>-elementen geen intrinsiek visueel vak hebben zonder browserbedieningselementen, kan een <audio loading="lazy">-element zonder controls de viewport niet zichtbaar kruisen en wordt het nooit geladen of automatisch afgespeeld.
  4. Kenmerken voor coördinaten preload en poster:
    • Op <video loading="lazy"> worden zowel het ophalen van de poster-afbeelding als het preload-gedrag (none, metadata of auto) uitgesteld totdat de video de viewport nadert.
    • Koppel loading="lazy" aan preload="none" als de gebruiker expliciet op Afspelen moet drukken nadat die naar de media heeft gescrold, of aan preload="metadata" als de duur en de gegevens van het eerste frame automatisch moeten worden ingevuld zodra de speler de viewport binnengaat.
  5. Lazy autoplay veilig verwerken: Als loading="lazy" wordt gecombineerd met autoplay op een gedempte <video> (of een opt-in <audio controls>), wordt het afspelen en downloaden uitgesteld totdat het element de viewport nadert. Respecteer altijd @media (prefers-reduced-motion: reduce) voordat je bewegingsmedia automatisch afspeelt.

Voorbeeldcode

<!-- 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 motion
const 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);
});

Praktische tips

  • VERPLICHT: Stel altijd expliciete kenmerken voor width en height in voor <video>-elementen. Niet-geladen video's hebben een intrinsieke grootte van 300×150 en worden nooit geladen als ze geen zichtbaar deel van de pagina overlappen.
  • VERPLICHT: Neem het kenmerk controls altijd op in <audio loading="lazy">-elementen. Lazy-loaded <audio>-elementen zonder controls worden niet zichtbaar weergegeven en worden nooit geladen of automatisch afgespeeld.
  • Pas loading="lazy" NIET toe op elementen boven de vouw of Largest Contentful Paint (LCP) <video>, omdat dit het ophalen van zowel de poster afbeeldings- als videogegevens vertraagt.
  • WEL: Laat het kenmerk loading weg als standaard eager loading gewenst is in plaats van expliciet loading="eager" te schrijven.
  • Vertrouw NIET op de gebeurtenis window load om te detecteren wanneer lazy-loaded <video>- of <audio>-elementen zijn geladen. De gebeurtenis load wordt alleen geactiveerd op basis van eager-loaded media, zelfs als een media-element loading="lazy" zich in eerste instantie in de visuele viewport bevindt.
  • Houd er rekening mee dat browsers loading="lazy"-media alleen uitstellen als JavaScript aanstaat (een maatregel tegen tracking om te voorkomen dat servers de scrollpositie afleiden via mediaverzoeken als scripting uitstaat).

Reservestrategie

Als je basislijndoel loading="lazy" niet ondersteunt op <video> en <audio>, kies je tussen progressive enhancement en een IntersectionObserver-fallback op basis van hoe strikt de bandbreedte moet worden bespaard:

Browsers die loading="lazy" op <video> en <audio> niet herkennen, negeren het kenmerk en vallen terug op standaard laden dat wordt beheerd door het kenmerk preload. Als je loading="lazy" koppelt aan preload="none" of preload="metadata", voorkom je dat niet-ondersteunde browsers het hele mediabestand vooraf downloaden, hoewel <video poster>-afbeeldingen en preload="metadata"-headers gretig worden opgehaald.

Reserve voor aangepaste code (IntersectionObserver)

Als je Baseline-doel vereist dat <video poster>-downloads, preload="metadata"-verzoeken of door de viewport geactiveerde weergave strikt worden uitgesteld in browsers zonder systeemeigen ondersteuning, voer je functiedetectie uit voor Object.hasOwn(HTMLMediaElement.prototype, 'loading') en val je terug op IntersectionObserver:

// Feature-detect native media lazy loading on HTMLMediaElement.prototype
const 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);
  }
}