Carga diferida de video y audio

Los elementos multimedia (<video> y <audio>) y sus recursos asociados (como las imágenes <video poster> y los metadatos precargados) consumen un ancho de banda de red y almacenamiento significativo durante la carga de página inicial. Incluso cuando se establece preload="metadata" o preload="none" en un <video>, el navegador recupera con entusiasmo su imagen poster de inmediato, y preload="metadata" sigue iniciando solicitudes de rango para los encabezados de contenido multimedia en todos los reproductores de la página.

Agregar loading="lazy" a los elementos <video> y <audio> fuera de la pantalla le indica al navegador que aplace todas las solicitudes de red de contenido multimedia, incluidas las descargas de imágenes poster, las sugerencias de preload y la ejecución de autoplay, hasta que el elemento alcance una distancia calculada por el navegador desde el viewport visual.

Cómo implementarlo

  1. Agrega loading="lazy" al contenido multimedia fuera de pantalla: Aplica loading="lazy" a los elementos <video> y <audio> ubicados en la mitad inferior de la página. Omite el atributo loading en el contenido multimedia que aparece en la mitad superior de la página para que se establezca de forma predeterminada en eager.
  2. Establece valores explícitos de width y height en los elementos <video>: Siempre especifica los atributos enteros de width y height (en píxeles de CSS) en <video>. Los videos que no se cargan tienen dimensiones predeterminadas de 300 × 150. Sin un tamaño explícito, es posible que un video de carga diferida no se cruce con el viewport visible y nunca se cargue, o bien que cause cambios de diseño disruptivos (CLS) cuando se cargue.
  3. Incluye el atributo controls en los elementos <audio>: Incluye siempre controls en <audio loading="lazy">. Debido a que los elementos <audio> no tienen un cuadro visual intrínseco sin controles del navegador, un elemento <audio loading="lazy"> sin controls no puede cruzarse de forma visible con el viewport y nunca se cargará ni se reproducirá automáticamente.
  4. Atributos preload y poster de Coordinate:
    • En <video loading="lazy">, tanto la recuperación de imágenes de poster como el comportamiento de preload (none, metadata o auto) se aplazan hasta que el video se acerca al viewport.
    • Vincula loading="lazy" con preload="none" cuando el usuario deba presionar Reproducir de forma explícita después de desplazarse hasta el contenido multimedia, o preload="metadata" cuando los datos de duración y fotograma inicial deban completarse automáticamente una vez que el reproductor ingrese al viewport.
  5. Controla autoplay diferido de forma segura: Cuando loading="lazy" se combina con autoplay en un <video> silenciado (o un <audio controls> de habilitación), la reproducción y la descarga esperan hasta que el elemento se acerque al viewport. Siempre respeta @media (prefers-reduced-motion: reduce) antes de reproducir automáticamente contenido multimedia en movimiento.

Ejemplo de código

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

Prácticas recomendadas

  • OBLIGATORIO: Siempre establece atributos width y height explícitos en los elementos <video>. Los videos que no se cargan tienen un tamaño intrínseco de 300×150 y nunca se cargarán si no se cruzan con una parte visible de la página.
  • OBLIGATORIO: Incluye siempre el atributo controls en los elementos <audio loading="lazy">. Los elementos <audio> de carga diferida sin controls no se renderizan de forma visible y nunca se cargarán ni se reproducirán automáticamente.
  • NO apliques loading="lazy" a los elementos <video> de la mitad superior de la página o de Largest Contentful Paint (LCP), ya que retrasa la recuperación de los datos de imagen y video de poster.
  • SÍ omitas el atributo loading cuando se desee la carga inmediata predeterminada en lugar de escribir explícitamente loading="eager".
  • NO confíes en el evento load de window para detectar cuándo se cargaron los elementos <video> o <audio> de carga diferida. El evento load se activa solo en función del contenido multimedia de carga inmediata, incluso si un elemento multimedia loading="lazy" se encuentra inicialmente dentro del viewport visual.
  • SÍ ten en cuenta que los navegadores solo aplazan el contenido multimedia loading="lazy" cuando JavaScript está habilitado (una medida antirastreo para evitar que los servidores infieran la posición de desplazamiento a través de solicitudes de contenido multimedia cuando las secuencias de comandos están inhabilitadas).

Estrategia de resguardo

Si tu objetivo de referencia no admite loading="lazy" en <video> y <audio>, elige entre la mejora progresiva y un resguardo de IntersectionObserver según el nivel de rigurosidad con el que se deba conservar el ancho de banda:

Los navegadores que no reconocen loading="lazy" en <video> y <audio> ignoran el atributo y recurren a la carga estándar regida por el atributo preload. La vinculación de loading="lazy" con preload="none" o preload="metadata" garantiza que los navegadores no compatibles eviten descargar el archivo multimedia completo por adelantado, aunque las imágenes <video poster> y los encabezados preload="metadata" se recuperarán con entusiasmo.

Resguardo de código personalizado (IntersectionObserver)

Cuando tu objetivo de Baseline requiera diferir estrictamente las descargas de <video poster>, las solicitudes de preload="metadata" o la reproducción activada por el viewport en navegadores sin compatibilidad nativa, detecta la función Object.hasOwn(HTMLMediaElement.prototype, 'loading') y, luego, recurre a 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);
  }
}