Chargement différé des vidéos et des contenus audio

Les éléments multimédias (<video> et <audio>) et leurs ressources associées (comme les images <video poster> et les métadonnées préchargées) consomment une bande passante réseau et de stockage importante lors du chargement de page initial. Même lorsque preload="metadata" ou preload="none" est défini sur un <video>, le navigateur récupère immédiatement son image poster, et preload="metadata" lance toujours des requêtes de plage pour les en-têtes multimédias sur chaque lecteur de la page.

L'ajout de loading="lazy" aux éléments <video> et <audio> hors écran indique au navigateur de différer toutes les requêtes réseau multimédias (y compris les téléchargements d'images poster, les indications preload et l'exécution de autoplay) jusqu'à ce que l'élément atteigne une distance calculée par le navigateur par rapport à la fenêtre d'affichage visuelle.

Mise en œuvre

  1. Ajouter loading="lazy" aux contenus multimédias hors écran : appliquez loading="lazy" aux éléments <video> et <audio> situés sous la ligne de flottaison. Omettez l'attribut loading sur les contenus multimédias au-dessus de la ligne de flottaison pour qu'il soit défini par défaut sur eager.
  2. Définissez explicitement les attributs width et height sur les éléments <video> : spécifiez toujours des attributs width et height entiers (en pixels CSS) sur <video>. Les vidéos non chargées ont par défaut des dimensions de 300 × 150. Sans dimensionnement explicite, une vidéo chargée en différé peut ne pas croiser la fenêtre d'affichage visible et ne jamais se charger, ou provoquer des décalages de mise en page (CLS) perturbants lors de son chargement.
  3. Incluez l'attribut controls dans les éléments <audio> : incluez toujours controls dans <audio loading="lazy">. Étant donné que les éléments <audio> n'ont pas de zone visuelle intrinsèque sans commandes du navigateur, un élément <audio loading="lazy"> sans controls ne peut pas croiser visiblement la fenêtre d'affichage et ne se chargera ni ne se lira jamais automatiquement.
  4. Attributs de coordonnées preload et poster :
    • Sur <video loading="lazy">, la récupération de l'image poster et le comportement preload (none, metadata ou auto) sont différés jusqu'à ce que la vidéo s'approche de la fenêtre d'affichage.
    • Associez loading="lazy" à preload="none" lorsque l'utilisateur doit appuyer explicitement sur le bouton de lecture après avoir fait défiler l'écran jusqu'au contenu multimédia, ou à preload="metadata" lorsque les données de durée et d'image initiale doivent être renseignées automatiquement une fois que le lecteur entre dans la fenêtre d'affichage.
  5. Gérer autoplay de manière sécurisée : lorsque loading="lazy" est combiné à autoplay sur un élément <video> sans son (ou un élément <audio controls> activé), la lecture et le téléchargement attendent que l'élément s'approche de la fenêtre d'affichage. Respectez toujours @media (prefers-reduced-motion: reduce) avant de lancer la lecture automatique de contenus multimédias animés.

Exemple de 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 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);
});

Bonnes pratiques

  • OBLIGATOIRE : définissez toujours des attributs width et height explicites sur les éléments <video>. Les vidéos non chargées ont une taille intrinsèque de 300 × 150 et ne se chargeront jamais si elles ne croisent pas une partie visible de la page.
  • OBLIGATOIRE : incluez toujours l'attribut controls dans les éléments <audio loading="lazy">. Les éléments <audio> à chargement différé sans controls ne sont pas affichés de manière visible et ne seront jamais chargés ni lus automatiquement.
  • N'appliquez PAS loading="lazy" aux éléments au-dessus de la ligne de flottaison ou Largest Contentful Paint (LCP) <video>, car cela retarde la récupération des données d'image et de vidéo poster.
  • À FAIRE : Omettez l'attribut loading lorsque le chargement hâtif par défaut est souhaité, plutôt que d'écrire explicitement loading="eager".
  • NE VOUS FIEZ PAS à l'événement window load pour détecter le chargement des éléments <video> ou <audio> à chargement différé. L'événement load ne se déclenche qu'en fonction des contenus multimédias chargés de manière hâtive, même si un élément multimédia loading="lazy" se trouve initialement dans la fenêtre d'affichage visuelle.
  • À NOTER : Les navigateurs ne diffèrent les contenus multimédias loading="lazy" que lorsque JavaScript est activé (mesure anti-suivi pour empêcher les serveurs de déduire la position de défilement via les requêtes multimédias lorsque les scripts sont désactivés).

Stratégie de remplacement

Si votre cible Baseline n'est pas compatible avec loading="lazy" sur <video> et <audio>, choisissez entre l'amélioration progressive et une solution de remplacement IntersectionObserver en fonction de la rigueur avec laquelle la bande passante doit être préservée :

Les navigateurs qui ne reconnaissent pas loading="lazy" sur <video> et <audio> ignorent l'attribut et reviennent au chargement standard régi par l'attribut preload. Associer loading="lazy" à preload="none" ou preload="metadata" permet d'éviter que les navigateurs non compatibles téléchargent l'intégralité du fichier multimédia à l'avance, même si les images <video poster> et les en-têtes preload="metadata" sont récupérés de manière hâtive.

Code de remplacement personnalisé (IntersectionObserver)

Lorsque votre cible Baseline nécessite de différer strictement les téléchargements <video poster>, les requêtes preload="metadata" ou la lecture déclenchée par la fenêtre d'affichage dans les navigateurs sans compatibilité native, détectez la fonctionnalité Object.hasOwn(HTMLMediaElement.prototype, 'loading'), puis utilisez 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);
  }
}