미디어 요소 (<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"가 음소거된<video>(또는 선택적<audio controls>)에서autoplay와 결합되면 요소가 표시 영역에 접근할 때까지 재생 및 다운로드가 대기합니다. 모션 미디어를 자동재생하기 전에 항상@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"를 스크롤 없이 볼 수 있는 부분 또는 최대 콘텐츠 페인트 (LCP)<video>요소에 적용하지 마세요.poster이미지와 동영상 데이터를 모두 가져오는 데 지연이 발생합니다.loading="eager"를 명시적으로 작성하는 대신 기본 즉시 로드를 원하는 경우loading속성을 생략해야 합니다.windowload이벤트에 의존하여 지연 로드된<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)
Baseline 타겟에서 기본 지원이 없는 브라우저에서 <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);
}
}