فيديوهات وملفات صوتية تظهر بأسلوب "التحميل الكسول"

تستهلك عناصر الوسائط (<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> التي يتم تحميلها بشكل بطيء بدون controls بشكل مرئي ولن يتم تحميلها أو تشغيلها تلقائيًا مطلقًا.
  • لا تطبّق السمة loading="lazy" على عناصر <video> في الجزء المرئي من الصفحة أو عناصر "سرعة عرض أكبر محتوى مرئي" (LCP)، لأنّ ذلك يؤدي إلى تأخير جلب بيانات كل من الصور والفيديوهات في poster.
  • يجب حذف السمة loading عندما يكون التحميل السريع التلقائي مطلوبًا بدلاً من كتابة loading="eager" بشكل صريح.
  • لا تعتمد على الحدث window load لرصد وقت تحميل عناصر <video> أو <audio> الكسولة التحميل. يتم تنشيط الحدث load استنادًا إلى الوسائط التي يتم تحميلها بشكل استباقي فقط، حتى إذا كان عنصر الوسائط loading="lazy" داخل إطار العرض المرئي في البداية.
  • يُرجى العِلم أنّ المتصفّحات تؤجّل فقط تحميل الوسائط loading="lazy" عندما تكون لغة JavaScript مفعَّلة (وهو إجراء لمكافحة التتبّع يمنع الخوادم من استنتاج موضع الانتقال من خلال طلبات الوسائط عندما تكون لغة البرمجة النصية غير مفعَّلة).

استراتيجية احتياطية

إذا كان هدفك الأساسي لا يتيح استخدام 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')"، يمكنك رصد الميزة 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);
  }
}