延迟加载视频和音频

媒体元素(<video> 和 <audio>)及其关联资源(例如 <video poster> 图片和预加载的元数据)在初始网页加载期间会消耗大量网络和存储带宽。即使在 <video> 上设置了 preload="metadata" 或 preload="none",浏览器也会立即主动提取其 poster 图片,并且 preload="metadata" 仍会针对网页上每个播放器的媒体标头发起范围请求。

向屏幕外 <video> 和 <audio> 元素添加 loading="lazy" 会指示浏览器推迟所有媒体网络请求(包括 poster 图片下载、preload 提示和 autoplay 执行),直到元素与视觉视口之间的距离达到浏览器计算的距离。

如何实现

  1. 为屏幕外媒体添加 loading="lazy":为位于非首屏的 <video> 和 <audio> 元素应用 loading="lazy"。省略首屏媒体上的 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. 坐标 preload 和 poster 属性:
    • 在 <video loading="lazy"> 上,poster 图片提取和 preload 行为(none、metadata 或 auto)都会推迟到视频接近视口时才执行。
    • 如果用户必须在滚动到媒体后明确按下播放键,请将 loading="lazy" 与 preload="none" 配对;如果播放器进入视口后,时长和初始帧数据应自动填充,请将 与 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 属性。没有 controls 的延迟加载 <audio> 元素不会以可见方式呈现,并且永远不会加载或自动播放。
  • 请勿将 loading="lazy" 应用于首屏或 Largest Contentful Paint (LCP) <video> 元素,因为这会延迟提取 poster 图片和视频数据。
  • 务必在需要默认 Eager 加载时省略 loading 属性,而不是明确写入 loading="eager"。
  • 请勿依赖 window load 事件来检测延迟加载的 <video> 或 <audio> 元素何时加载。load 事件仅根据预先加载的媒体触发,即使 loading="lazy" 媒体元素最初位于可视视口内也是如此。
  • 请注意,只有在启用 JavaScript 的情况下,浏览器才会延迟加载 loading="lazy" 媒体(这是一种反跟踪措施,旨在防止服务器在停用脚本时通过媒体请求推断滚动位置)。

后备策略

如果您的基准目标不支持 loading="lazy"(在 <video> 和 <audio> 上),请根据必须严格节省带宽的程度,在渐进增强和 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);
  }
}