Tải chậm video và âm thanh

Các phần tử đa phương tiện (<video> và <audio>) và tài nguyên liên quan (chẳng hạn như hình ảnh <video poster> và siêu dữ liệu được tải trước) tiêu tốn đáng kể băng thông mạng và bộ nhớ trong quá trình tải trang ban đầu. Ngay cả khi preload="metadata" hoặc preload="none" được đặt trên <video>, trình duyệt vẫn sẽ tìm nạp ngay hình ảnh poster và preload="metadata" vẫn bắt đầu các yêu cầu về dải ô cho tiêu đề nội dung nghe nhìn trên mọi trình phát trên trang.

Việc thêm loading="lazy" vào các phần tử <video> và <audio> ngoài màn hình sẽ hướng dẫn trình duyệt trì hoãn tất cả yêu cầu mạng về nội dung nghe nhìn (bao gồm cả việc tải hình ảnh poster xuống, gợi ý preload và thực thi autoplay) cho đến khi phần tử đạt đến khoảng cách được trình duyệt tính toán từ khung nhìn trực quan.

Cách triển khai

  1. Thêm loading="lazy" vào nội dung nghe nhìn ngoài màn hình: Áp dụng loading="lazy" cho các phần tử <video> và <audio> nằm dưới màn hình đầu tiên. Bỏ qua thuộc tính loading trên nội dung nghe nhìn trong màn hình đầu tiên để thuộc tính này mặc định là eager.
  2. Đặt width và height rõ ràng trên các phần tử <video>: Luôn chỉ định các thuộc tính width và height là số nguyên (tính bằng pixel CSS) trên <video>. Video chưa tải có kích thước mặc định là 300×150; nếu không có kích thước rõ ràng, video tải từng phần có thể không giao nhau với khung nhìn hiển thị và không bao giờ tải, hoặc gây ra sự thay đổi bố cục gây gián đoạn (CLS) khi tải.
  3. Thêm thuộc tính controls vào các phần tử <audio>: Luôn thêm controls vào <audio loading="lazy">. Vì các phần tử <audio> không có hộp hình ảnh nội tại nếu không có các chế độ điều khiển của trình duyệt, nên phần tử <audio loading="lazy"> không có controls không thể giao nhau một cách rõ ràng với khung nhìn và sẽ không bao giờ tải hoặc tự động phát.
  4. Thuộc tính toạ độ preload và poster:
    • Trên <video loading="lazy">, cả hoạt động tìm nạp hình ảnh poster và hành vi preload (none, metadata hoặc auto) đều bị trì hoãn cho đến khi video tiếp cận khung nhìn.
    • Ghép nối loading="lazy" với preload="none" khi người dùng phải nhấn nút phát một cách rõ ràng sau khi di chuyển đến nội dung nghe nhìn, hoặc preload="metadata" khi thời lượng và dữ liệu khung hình ban đầu sẽ tự động điền sau khi trình phát vào khung nhìn.
  5. Xử lý autoplay tải từng phần một cách an toàn: Khi loading="lazy" được kết hợp với autoplay trên một <video> bị tắt tiếng (hoặc <audio controls> chọn sử dụng), quá trình phát và tải xuống sẽ chờ cho đến khi phần tử tiếp cận khung nhìn. Luôn tuân thủ @media (prefers-reduced-motion: reduce) trước khi tự động phát nội dung nghe nhìn chuyển động.

Mã mẫu

<!-- 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);
});

Các phương pháp hay nhất

  • BẮT BUỘC: Luôn đặt rõ ràng các thuộc tính width và height trên các phần tử <video>. Video chưa tải có kích thước nội tại là 300×150 và sẽ không bao giờ tải nếu không giao nhau với một phần hiển thị của trang.
  • BẮT BUỘC: Luôn thêm thuộc tính controls vào các phần tử <audio loading="lazy">. Các phần tử <audio> được tải từng phần mà không có controls sẽ không được hiển thị và sẽ không bao giờ tải hoặc tự động phát.
  • KHÔNG áp dụng loading="lazy" cho các phần tử trong màn hình đầu tiên hoặc Nội dung lớn nhất hiển thị (LCP) <video>, vì việc này sẽ làm chậm quá trình tìm nạp cả dữ liệu hình ảnh và video poster.
  • NÊN bỏ qua thuộc tính loading khi muốn tải nhanh mặc định thay vì viết rõ ràng loading="eager".
  • KHÔNG dựa vào sự kiện window load để phát hiện thời điểm các phần tử <video> hoặc <audio> tải từng phần đã tải xong. Sự kiện load chỉ kích hoạt dựa trên nội dung nghe nhìn được tải sớm, ngay cả khi phần tử nghe nhìn loading="lazy" ban đầu nằm trong khung nhìn trực quan.
  • LƯU Ý rằng trình duyệt chỉ trì hoãn nội dung nghe nhìn loading="lazy" khi JavaScript được bật (một biện pháp chống theo dõi để ngăn máy chủ suy luận vị trí cuộn thông qua các yêu cầu nội dung nghe nhìn khi tính năng viết tập lệnh bị tắt).

Chiến lược dự phòng

Nếu mục tiêu Baseline của bạn không hỗ trợ loading="lazy" trên <video> và <audio>, hãy chọn giữa cải tiến tăng dần và phương án dự phòng IntersectionObserver dựa trên mức độ nghiêm ngặt của việc bảo toàn băng thông:

Các trình duyệt không nhận dạng được loading="lazy" trên <video> và <audio> sẽ bỏ qua thuộc tính này và quay lại chế độ tải tiêu chuẩn chịu sự điều chỉnh của thuộc tính preload. Việc ghép nối loading="lazy" với preload="none" hoặc preload="metadata" đảm bảo các trình duyệt không được hỗ trợ vẫn tránh tải trước toàn bộ tệp nội dung nghe nhìn, mặc dù hình ảnh <video poster> và tiêu đề preload="metadata" sẽ được tìm nạp nhanh chóng.

Mã dự phòng tuỳ chỉnh (IntersectionObserver)

Khi mục tiêu Đường cơ sở yêu cầu trì hoãn nghiêm ngặt việc tải <video poster> xuống, yêu cầu preload="metadata" hoặc phát do khung nhìn kích hoạt trong các trình duyệt không có tính năng hỗ trợ gốc, hãy phát hiện tính năng Object.hasOwn(HTMLMediaElement.prototype, 'loading'), sau đó quay lại 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);
  }
}