Vídeo e áudio com carregamento lento

Os elementos de mídia (<video> e <audio>) e os recursos associados (como imagens <video poster> e metadados pré-carregados) consomem uma largura de banda significativa de rede e armazenamento durante o carregamento de página inicial. Mesmo quando preload="metadata" ou preload="none" é definido em um <video>, o navegador busca imediatamente a imagem poster, e preload="metadata" ainda inicia solicitações de intervalo para cabeçalhos de mídia em todos os players da página.

Adicionar loading="lazy" aos elementos <video> e <audio> fora da tela instrui o navegador a adiar todas as solicitações de rede de mídia, incluindo downloads de imagens poster, dicas de preload e execução de autoplay, até que o elemento alcance uma distância calculada pelo navegador da janela de visualização.

Como fazer a implementação

  1. Adicione loading="lazy" à mídia fora da tela: aplique loading="lazy" aos elementos <video> e <audio> posicionados abaixo da dobra. Omita o atributo loading na mídia acima da dobra para que o padrão seja eager.
  2. Defina width e height explícitos em elementos <video>: sempre especifique os atributos inteiros width e height (em pixels CSS) em <video>. Os vídeos não carregados têm dimensões padrão de 300×150. Sem um dimensionamento explícito, um vídeo com carregamento lento pode não se cruzar com a janela de visualização visível e nunca carregar, ou causar mudanças cumulativas de layout (CLS) ao carregar.
  3. Inclua o atributo controls nos elementos <audio>: sempre inclua controls em <audio loading="lazy">. Como os elementos <audio> não têm uma caixa visual intrínseca sem controles do navegador, um elemento <audio loading="lazy"> sem controls não pode cruzar visivelmente a janela de visualização e nunca será carregado ou reproduzido automaticamente.
  4. Atributos de coordenadas preload e poster:
    • No <video loading="lazy">, a busca de imagens poster e o comportamento preload (none, metadata ou auto) são adiados até que o vídeo se aproxime da janela de visualização.
    • Combine loading="lazy" com preload="none" quando o usuário precisar pressionar explicitamente o botão de reprodução depois de rolar até a mídia ou preload="metadata" quando os dados de duração e frame inicial precisarem ser preenchidos automaticamente assim que o player entrar na janela de visualização.
  5. Processar autoplay lento com segurança: quando loading="lazy" é combinado com autoplay em um <video> silenciado (ou um <audio controls> de ativação), a reprodução e o download aguardam até que o elemento se aproxime da janela de visualização. Sempre respeite @media (prefers-reduced-motion: reduce) antes de reproduzir automaticamente mídia em movimento.

Exemplo 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áticas recomendadas

  • OBRIGATÓRIO: sempre defina atributos width e height explícitos em elementos <video>. Os vídeos não carregados têm um tamanho intrínseco de 300×150 e nunca serão carregados se não cruzarem uma parte visível da página.
  • OBRIGATÓRIO: sempre inclua o atributo controls nos elementos <audio loading="lazy">. Os elementos <audio> de carregamento lento sem controls não são renderizados de forma visível e nunca serão carregados ou reproduzidos automaticamente.
  • NÃO aplique loading="lazy" a elementos acima da dobra ou de Largest Contentful Paint (LCP) <video>, porque isso atrasa a busca de dados de imagem e vídeo do poster.
  • OMITA o atributo loading quando o carregamento antecipado padrão for desejado, em vez de escrever explicitamente loading="eager".
  • NÃO use o evento window load para detectar quando os elementos <video> ou <audio> de carregamento lento foram carregados. O evento load é acionado com base apenas em mídia carregada de forma antecipada, mesmo que um elemento de mídia loading="lazy" esteja inicialmente dentro da janela de visualização.
  • OBSERVE que os navegadores só adiam a mídia loading="lazy" quando o JavaScript está ativado (uma medida de combate ao rastreamento para impedir que os servidores infiram a posição de rolagem por solicitações de mídia quando o script está desativado).

Estratégia de fallback

Se o Baseline de destino não for compatível com loading="lazy" em <video> e <audio>, escolha entre o aprimoramento progressivo e um substituto de IntersectionObserver com base no rigor da conservação da largura de banda:

Os navegadores que não reconhecem loading="lazy" em <video> e <audio> ignoram o atributo e usam o carregamento padrão regido pelo atributo preload. Ao combinar loading="lazy" com preload="none" ou preload="metadata", os navegadores sem suporte ainda evitam o download do arquivo de mídia completo, embora as imagens <video poster> e os cabeçalhos preload="metadata" sejam buscados com antecedência.

Fallback de código personalizado (IntersectionObserver)

Quando sua meta de Baseline exige o adiamento estrito de downloads de <video poster>, solicitações de preload="metadata" ou reprodução acionada pela janela de visualização em navegadores sem suporte nativo, use a detecção de recursos para Object.hasOwn(HTMLMediaElement.prototype, 'loading') e, em seguida, use IntersectionObserver como alternativa:

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