องค์ประกอบสื่อ (<video> และ <audio>) และทรัพยากรที่เกี่ยวข้อง (เช่น รูปภาพ <video poster> และข้อมูลเมตาที่โหลดไว้ล่วงหน้า) ใช้แบนด์วิดท์เครือข่ายและพื้นที่เก็บข้อมูลจำนวนมากในระหว่างการโหลดหน้าเว็บครั้งแรก แม้ว่าจะตั้งค่า preload="metadata" หรือ preload="none" ไว้ใน <video> เบราว์เซอร์จะดึงข้อมูลรูปภาพ poster ทันที และ preload="metadata" จะยังคงเริ่มคำขอช่วงสำหรับส่วนหัวของสื่อในโปรแกรมเล่นทุกโปรแกรมในหน้าเว็บ
การเพิ่ม loading="lazy" ลงในองค์ประกอบ <video> และ <audio> นอกหน้าจอจะสั่งให้เบราว์เซอร์เลื่อนคำขอเครือข่ายสื่อทั้งหมดออกไป ซึ่งรวมถึงการดาวน์โหลดรูปภาพ poster, คำแนะนำ preload และการดำเนินการ autoplay จนกว่าองค์ประกอบจะถึงระยะห่างที่เบราว์เซอร์คำนวณจากวิวพอร์ตภาพ
วิธีการนำไปใช้งาน
- เพิ่ม
loading="lazy"ลงในสื่อที่อยู่นอกหน้าจอ: ใช้loading="lazy"กับองค์ประกอบ<video>และ<audio>ที่อยู่ด้านล่างครึ่งหน้าบน ละเว้นแอตทริบิวต์loadingในสื่อครึ่งหน้าบนเพื่อให้มีค่าเริ่มต้นเป็นeager - ตั้งค่า
widthและheightที่ชัดเจนในองค์ประกอบ<video>: ระบุแอตทริบิวต์widthและheightเป็นจำนวนเต็ม (ในพิกเซล CSS) ใน<video>เสมอ วิดีโอที่ไม่ได้โหลดจะมีขนาดเริ่มต้นเป็น300×150หากไม่มีการกำหนดขนาดที่ชัดเจน วิดีโอที่โหลดแบบ Lazy Loading อาจไม่ตัดกับวิวพอร์ตที่มองเห็นได้และไม่โหลดเลย หรือทำให้เกิดการเปลี่ยนแปลงเลย์เอาต์ (CLS) ที่รบกวนเมื่อโหลด - ใส่แอตทริบิวต์
controlsในองค์ประกอบ<audio>: ใส่controlsใน<audio loading="lazy">เสมอ เนื่องจากองค์ประกอบ<audio>ไม่มีกล่องภาพที่แท้จริงหากไม่มีการควบคุมเบราว์เซอร์ องค์ประกอบ<audio loading="lazy">ที่ไม่มีcontrolsจึงไม่สามารถตัดกับวิวพอร์ตได้อย่างชัดเจน และจะไม่โหลดหรือเล่นอัตโนมัติ - แอตทริบิวต์พิกัด
preloadและposter- ใน
<video loading="lazy">ระบบจะเลื่อนทั้งการดึงข้อมูลรูปภาพposterและลักษณะการทำงานpreload(none,metadataหรือauto) ออกไปจนกว่าวิดีโอจะเข้าใกล้วิวพอร์ต - จับคู่
loading="lazy"กับpreload="none"เมื่อผู้ใช้ต้องกดเล่นอย่างชัดเจนหลังจากเลื่อนไปที่สื่อ หรือpreload="metadata"เมื่อระยะเวลาและข้อมูลเฟรมเริ่มต้นควรป้อนโดยอัตโนมัติเมื่อเพลเยอร์เข้าสู่วิวพอร์ต
- ใน
- จัดการ
autoplayแบบ Lazy อย่างปลอดภัย: เมื่อใช้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);
});
แนวทางปฏิบัติแนะนำ
- บังคับ: ตั้งค่าแอตทริบิวต์
widthและheightที่ชัดเจนในองค์ประกอบ<video>เสมอ วิดีโอที่ไม่ได้โหลดจะมีขนาดจริง300×150และจะไม่โหลดหากไม่ตัดกับส่วนที่มองเห็นได้ของหน้าเว็บ - จำเป็น: ใส่แอตทริบิวต์
controlsในองค์ประกอบ<audio loading="lazy">เสมอ องค์ประกอบ<audio>ที่โหลดแบบ Lazy Loading โดยไม่มีcontrolsจะไม่แสดงผลให้เห็น และจะไม่โหลดหรือเล่นอัตโนมัติ - อย่าใช้
loading="lazy"กับองค์ประกอบ<video>ที่อยู่ครึ่งหน้าบนหรือ Largest Contentful Paint (LCP) เนื่องจากจะทำให้การดึงข้อมูลทั้งรูปภาพและวิดีโอposterล่าช้า - ควรละเว้นแอตทริบิวต์
loadingเมื่อต้องการใช้การโหลดแบบ Eager โดยค่าเริ่มต้น แทนที่จะเขียนloading="eager"อย่างชัดแจ้ง - อย่าใช้เหตุการณ์
windowloadเพื่อตรวจหาว่าองค์ประกอบ<video>หรือ<audio>ที่โหลดแบบ Lazy Loading โหลดเสร็จแล้วหรือไม่ เหตุการณ์loadจะเริ่มทำงานตามสื่อที่โหลดแบบกระตือรือร้นเท่านั้น แม้ว่าองค์ประกอบสื่อloading="lazy"จะอยู่ในวิวพอร์ตภาพตั้งแต่แรกก็ตาม - ควรทราบว่าเบราว์เซอร์จะเลื่อนเวลาโหลดสื่อ
loading="lazy"เฉพาะเมื่อเปิดใช้ JavaScript เท่านั้น (มาตรการต่อต้านการติดตามเพื่อป้องกันไม่ให้เซิร์ฟเวอร์อนุมานตำแหน่งการเลื่อนผ่านคำขอสื่อเมื่อปิดใช้การเขียนสคริปต์)
กลยุทธ์สำรอง
หาก Baseline เป้าหมายไม่รองรับ loading="lazy" ใน <video> และ <audio> ให้เลือกระหว่างการเพิ่มประสิทธิภาพแบบต่อเนื่องกับ IntersectionObserver สำรอง โดยพิจารณาจากความเข้มงวดในการประหยัดแบนด์วิดท์ ดังนี้
การเพิ่มประสิทธิภาพแบบต่อเนื่อง (ค่าเริ่มต้นที่แนะนำ)
เบราว์เซอร์ที่ไม่รู้จัก loading="lazy" ใน <video> และ <audio> จะไม่สนใจแอตทริบิวต์ดังกล่าวและกลับไปใช้การโหลดมาตรฐานที่ควบคุมโดยแอตทริบิวต์ 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);
}
}