วิดีโอและเสียงที่โหลดแบบ Lazy Loading

องค์ประกอบสื่อ (<video> และ <audio>) และทรัพยากรที่เกี่ยวข้อง (เช่น รูปภาพ <video poster> และข้อมูลเมตาที่โหลดไว้ล่วงหน้า) ใช้แบนด์วิดท์เครือข่ายและพื้นที่เก็บข้อมูลจำนวนมากในระหว่างการโหลดหน้าเว็บครั้งแรก แม้ว่าจะตั้งค่า preload="metadata" หรือ preload="none" ไว้ใน <video> เบราว์เซอร์จะดึงข้อมูลรูปภาพ poster ทันที และ preload="metadata" จะยังคงเริ่มคำขอช่วงสำหรับส่วนหัวของสื่อในโปรแกรมเล่นทุกโปรแกรมในหน้าเว็บ

การเพิ่ม loading="lazy" ลงในองค์ประกอบ <video> และ <audio> นอกหน้าจอจะสั่งให้เบราว์เซอร์เลื่อนคำขอเครือข่ายสื่อทั้งหมดออกไป ซึ่งรวมถึงการดาวน์โหลดรูปภาพ poster, คำแนะนำ preload และการดำเนินการ autoplay จนกว่าองค์ประกอบจะถึงระยะห่างที่เบราว์เซอร์คำนวณจากวิวพอร์ตภาพ

วิธีการนำไปใช้งาน

  1. เพิ่ม loading="lazy" ลงในสื่อที่อยู่นอกหน้าจอ: ใช้ loading="lazy" กับองค์ประกอบ <video> และ <audio> ที่อยู่ด้านล่างครึ่งหน้าบน ละเว้นแอตทริบิวต์ loading ในสื่อครึ่งหน้าบนเพื่อให้มีค่าเริ่มต้นเป็น eager
  2. ตั้งค่า width และ height ที่ชัดเจนในองค์ประกอบ <video>: ระบุแอตทริบิวต์ width และ height เป็นจำนวนเต็ม (ในพิกเซล CSS) ใน <video> เสมอ วิดีโอที่ไม่ได้โหลดจะมีขนาดเริ่มต้นเป็น 300×150 หากไม่มีการกำหนดขนาดที่ชัดเจน วิดีโอที่โหลดแบบ Lazy Loading อาจไม่ตัดกับวิวพอร์ตที่มองเห็นได้และไม่โหลดเลย หรือทำให้เกิดการเปลี่ยนแปลงเลย์เอาต์ (CLS) ที่รบกวนเมื่อโหลด
  3. ใส่แอตทริบิวต์ controls ในองค์ประกอบ <audio>: ใส่ controls ใน <audio loading="lazy"> เสมอ เนื่องจากองค์ประกอบ <audio> ไม่มีกล่องภาพที่แท้จริงหากไม่มีการควบคุมเบราว์เซอร์ องค์ประกอบ <audio loading="lazy"> ที่ไม่มี controls จึงไม่สามารถตัดกับวิวพอร์ตได้อย่างชัดเจน และจะไม่โหลดหรือเล่นอัตโนมัติ
  4. แอตทริบิวต์พิกัด preload และ poster
    • ใน <video loading="lazy"> ระบบจะเลื่อนทั้งการดึงข้อมูลรูปภาพ poster และลักษณะการทำงาน preload (none, metadata หรือ auto) ออกไปจนกว่าวิดีโอจะเข้าใกล้วิวพอร์ต
    • จับคู่ loading="lazy" กับ preload="none" เมื่อผู้ใช้ต้องกดเล่นอย่างชัดเจนหลังจากเลื่อนไปที่สื่อ หรือ preload="metadata" เมื่อระยะเวลาและข้อมูลเฟรมเริ่มต้นควรป้อนโดยอัตโนมัติเมื่อเพลเยอร์เข้าสู่วิวพอร์ต
  5. จัดการ 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" อย่างชัดแจ้ง
  • อย่าใช้เหตุการณ์ window load เพื่อตรวจหาว่าองค์ประกอบ <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);
  }
}