延遲載入影片和音訊

媒體元素 (<video> 和 <audio>) 及其相關資源 (例如 <video poster> 圖片和預先載入的中繼資料),在初始載入網頁期間會耗用大量網路和儲存空間頻寬。即使在 <video> 上設定 preload="metadata" 或 preload="none",瀏覽器仍會立即擷取其 poster 圖片,且 preload="metadata" 仍會針對頁面上每個播放器的媒體標頭發出範圍要求。

將 loading="lazy" 新增至畫面外的 <video> 和 <audio> 元素,可指示瀏覽器延遲所有媒體網路要求,包括 poster 圖片下載、preload 提示和 autoplay 執行,直到元素與視覺可視區域的距離達到瀏覽器計算的距離。

導入方式

  1. 為畫面外媒體新增 loading="lazy":將 loading="lazy" 套用至不需捲動位置下方的 <video> 和 <audio> 元素。省略不需捲動位置媒體的 loading 屬性,讓系統預設為 eager。
  2. 為 <video> 元素設定明確的 width 和 height:請務必在 <video> 上指定整數 width 和 height 屬性 (以 CSS 像素為單位)。未載入的影片預設尺寸為 300×150。如果未明確指定大小,延遲載入的影片可能無法與可見的可視區域交集,因此永遠不會載入,或是在載入時造成干擾性的版面配置位移 (CLS)。
  3. 在 <audio> 元素中加入 controls 屬性:請務必在 <audio loading="lazy"> 中加入 controls。由於 <audio> 元素沒有瀏覽器控制項,因此沒有內建的視覺方塊,沒有 controls 的 <audio loading="lazy"> 元素無法與可視區域交集,因此永遠不會載入或自動播放。
  4. Coordinate preload 和 poster 屬性:
    • 在 <video loading="lazy">,poster 圖片擷取和 preload 行為 (none、metadata 或 auto) 都會延遲,直到影片接近可視區域。
    • 如果使用者必須在捲動至媒體後明確按下播放,請將 loading="lazy" 與 preload="none" 搭配使用;如果播放器進入可視區域後,應自動填入時間長度和初始影格資料,請將 loading="lazy" 與 preload="metadata" 搭配使用。
  5. 安全處理延遲 autoplay:如果 loading="lazy" 與 autoplay 結合,用於靜音的 <video> (或選擇加入的 <audio controls>),則播放和下載作業會等到元素接近可視區域時才開始。自動播放動態媒體前,請務必遵循 @media (prefers-reduced-motion: reduce)。

範例程式碼

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

最佳做法

  • 必要:請務必在 <video> 元素上設定明確的 width 和 height 屬性。未載入的影片固有大小為 300×150,如果未與網頁的可見部分交集,就永遠不會載入。
  • 必要:請務必在 <audio loading="lazy"> 元素中加入 controls 屬性。如果延遲載入的 <audio> 元素沒有 controls,就不會顯示,也無法載入或自動播放。
  • 請勿將 loading="lazy" 套用至不需捲動位置或 Largest Contentful Paint (LCP) <video> 元素,因為這會延遲擷取 poster 圖片和影片資料。
  • 務必省略 loading 屬性,而非明確寫入 loading="eager",以採用預設的積極載入。
  • 請勿依賴 window load 事件偵測延遲載入的 <video> 或 <audio> 元素何時載入。load 事件只會根據積極載入的媒體觸發,即使 loading="lazy" 媒體元素一開始位於可視區域內也一樣。
  • 務必注意,瀏覽器只有在啟用 JavaScript 時才會延遲 loading="lazy" 媒體 (這是一種反追蹤措施,可防止伺服器在停用指令碼時,透過媒體要求推斷捲動位置)。

備用策略

如果基準目標不支援 <video> 和 <audio> 上的 loading="lazy",請根據頻寬節省的嚴格程度,選擇漸進式增強或 IntersectionObserver 備用方案:

如果瀏覽器無法辨識 <video> 和 <audio> 上的 loading="lazy",就會忽略該屬性,並改用 preload 屬性控管的標準載入方式。將 loading="lazy" 與 preload="none" 或 preload="metadata" 配對,可確保不支援的瀏覽器仍能避免預先下載完整的媒體檔案,但系統會積極擷取 <video poster> 圖片和 preload="metadata" 標頭。

自訂程式碼備用廣告 (IntersectionObserver)

如果基準目標要求在不支援原生的瀏覽器中,嚴格延遲 <video poster> 下載、preload="metadata" 要求或可視區域觸發的播放,請偵測 Object.hasOwn(HTMLMediaElement.prototype, 'loading') 功能,然後改用 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);
  }
}