טעינה מדורגת של וידאו ואודיו

רכיבי מדיה (<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. בלי הגדרת גודל מפורשת, יכול להיות שסרטון בטעינה מדורגת לא יחצה את אזור התצוגה הגלוי ולעולם לא ייטען, או שהוא יגרום לשינויי פריסה משבשים (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 באופן בטוח: כשמשלבים את 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" על רכיבים בחלק העליון של הדף או על רכיבי LCP (המהירות שבה נטען רכיב התוכן הכי גדול) <video>, כי זה מעכב את השליפה של נתוני התמונות והסרטונים של poster.
  • מומלץ להשמיט את המאפיין loading כשרוצים להשתמש בטעינה מוקדמת כברירת מחדל, במקום לכתוב במפורש loading="eager".
  • אל תסתמכו על האירוע window load כדי לזהות מתי רכיבי <video> או <audio> בטעינה מדורגת נטענו. האירוע load מופעל רק על סמך מדיה שנטענת באופן מיידי, גם אם רכיב מדיה מסוג loading="lazy" נמצא בהתחלה בתוך אזור התצוגה החזותי.
  • חשוב לשים לב שדפדפנים דוחים מדיה מסוג loading="lazy" רק כש-JavaScript מופעל (אמצעי למניעת מעקב שמונע משרתים להסיק את מיקום הגלילה באמצעות בקשות מדיה כש-scripting מושבת).

אסטרטגיית גיבוי

אם יעד ה-Baseline לא תומך ב-loading="lazy" ב-<video> וב-<audio>, צריך לבחור בין שיפור הדרגתי (progressive enhancement) לבין חלופה של 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);
  }
}