媒体元素(<video> 和 <audio>)及其关联资源(例如 <video poster> 图片和预加载的元数据)在初始网页加载期间会消耗大量网络和存储带宽。即使在 <video> 上设置了 preload="metadata" 或 preload="none",浏览器也会立即主动提取其 poster 图片,并且 preload="metadata" 仍会针对网页上每个播放器的媒体标头发起范围请求。
向屏幕外 <video> 和 <audio> 元素添加 loading="lazy" 会指示浏览器推迟所有媒体网络请求(包括 poster 图片下载、preload 提示和 autoplay 执行),直到元素与视觉视口之间的距离达到浏览器计算的距离。
如何实现
- 为屏幕外媒体添加
loading="lazy":为位于非首屏的<video>和<audio>元素应用loading="lazy"。省略首屏媒体上的loading属性,使其默认为eager。 - 在
<video>元素上设置明确的width和height:请务必在<video>上指定整数width和height属性(以 CSS 像素为单位)。未加载的视频默认尺寸为300×150;如果没有明确的尺寸,延迟加载的视频可能无法与可见视口相交,从而永远无法加载,或者在加载时导致破坏性的布局偏移 (CLS)。 - 在
<audio>元素中添加controls属性:请务必在<audio loading="lazy">中添加controls。由于<audio>元素在没有浏览器控件的情况下没有固有的视觉框,因此没有controls的<audio loading="lazy">元素无法在视觉上与视口相交,并且永远不会加载或自动播放。 - 坐标
preload和poster属性:- 在
<video loading="lazy">上,poster图片提取和preload行为(none、metadata或auto)都会推迟到视频接近视口时才执行。 - 如果用户必须在滚动到媒体后明确按下播放键,请将
loading="lazy"与preload="none"配对;如果播放器进入视口后,时长和初始帧数据应自动填充,请将 与preload="metadata"配对。
- 在
- 安全地处理延迟
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"。 - 请勿依赖
windowload事件来检测延迟加载的<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);
}
}