When you lazy load a video, you postpone loading the video until playback is required. The opposite - preloading - stresses the page load speed and data cap of your viewers.

This article lists techniques for on-demand loading, and describes how to apply these techniques to a HTML <video>, embedded video players (e.g. YouTube) and JavaScript video players.

© Image by Google: "Automatically lazy-loading offscreen images & iframes for Lite mode users"

Techniques

You can combine the following techniques to implement lazy loading in your web app.

<video> API

Let's analyse the following <video> tag and its attributes.

<video preload="none" src="video.mp4" 
       autoplay="false" poster="poster.jpg"
       muted="false" loop="false"
       width="100%" height="100%">
</video>

The src, preload, autoplay and poster attributes are related to lazy loading.

  • src sets the video source. You can also configure this through a <source> node instead.
  • preload preloads data. If your src is configured, set preload to none to avoid preloading. If src (or <source>) isn't configured, you can use preload="auto" because preloading won't start until there's an associated video source.
  • autoplay starts playing the video automatically, which means the video download starts.
  • poster sets a placeholder image until video playback starts. This placeholder image must also be downloaded, so you also want to lazy load the poster if possible.

You can find a list of all video attributes at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video.

You can use the HTMLMediaElement interface to manipulate the <video> element with JavaScript, after obtaining a reference to the <video> element (e.g. video = document.querySelector('video')). The following properties and methods are related to lazy loading:

  • video.load() loads some initial video data.
  • video.play() starts playback.
  • video.preload = "auto" indicates that some data must be preloaded.

Intersection Observer API

The Intersection Observer API allows developers to programmatically detect the visibility of a DOM element – and also a video element or iframe.

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
let target = document.querySelector('#listItem');
observer.observe(target);

As the snippet indicates, you can observe the visibility of a targeted DOM element. You can pass along a callback function to execute logic when the target is visible – for example, you could preload the video.

The alternative? Use scroll, resize, or orientationchange event handlers

Async JavaScript

You can make your JavaScript files load asynchronously by using the async attribute.

<script src="video-player.js" async></script>

When you mark a script as async, your JavaScript is no longer parser blocking. When your script is parser blocking, the web browser pauses DOM construction when encountering your script. (In layman's terms: your viewers get a shitty browsing experience because your page pauses loading text, containers, etc. for a bit.)

Lazy Loading Libraries

Why re-invent the wheel? There are a number of JavaScript libraries which combine the mentioned techniques.

  • LazyLoad uses the Intersection Observer API and can be used to lazy load videos, images and iframes. The library supports async loading.
  • lazyYT is jQuery library to lazy load YouTube iframes.
  • Lazy load XT is jQuery library to lazy load videos, (YouTube) iframes and images.

Alternatives

Wouldn't it be awesome if there was a native lazy loading API? Something like <video loading="lazy" src="video.mp4" poster="poster.jpg"></video> for example? Well, as of April 2020, the loading="lazy" attribute is actually supported on Chromium and Firefox for <img> tags. The attribute is also supported for <iframe> tags on Chromium. There's currently no support for <video> tags though.

HTML Video

The following snippet uses the <video> API and Intersection Observer API to achieve lazy loading.

Quick heads-up: there's a video below the snippet. This video starts lazy loading when the video is visible.

...
<video autoplay="false" muted loop 
       playsinline width="100%" height="auto" 
       data-poster="https://ottball.com/content/images/2020/06/image-3.png" 
       class="lazy">
  <source data-src="https://storage.googleapis.com/ottball-b-1/videos/tos/youcompress.mp4" 
          type="video/mp4">
</video>
...
<script>
document.addEventListener("DOMContentLoaded", function() {
  var videos = [].slice.call(document.querySelectorAll("video.lazy"));

  if ("IntersectionObserver" in window) {
    var videoObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(video) {
        if (video.isIntersecting) {
          video.poster = video.dataset.poster;
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

          video.target.load();
          video.target.classList.remove("lazy");
          videoObserver.unobserve(video.target);
        }
      });
    });

    videos.forEach(function(video) {
      videoObserver.observe(video);
    });
  }
});
</script>
...

We adapted this snippet from Google's Developers Blog.

The following snippet could be used to achieve lazy loading through the LazyLoad library.

<video
  class="lazy"
  controls
  width="100%"
  data-src="https://storage.googleapis.com/ottball-b-1/videos/tos/youcompress.mp4"
  data-poster="https://ottball.com/content/images/2020/06/image-3.png">
  <source type="video/mp4"
      data-src="https://storage.googleapis.com/ottball-b-1/videos/tos/youcompress.mp4" />
</video>
<script>
  window.lazyLoadOptions = {
    elements_selector: ".lazy"
  };
</script>
<script
  async
  src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@16.1.0/dist/lazyload.min.js">
</script>

Embedded Players

The following snippet uses the Intersection Observer API to achieve lazy loading for iframes. We'll use YouTube as our sample case.

Another quick heads-up: there's an iframe below the snippet. This iframe starts lazy loading when the iframe is visible.

...
<style>
    .iframe-container {
        position: relative;
        width: 100%;
        height: 0;
        padding-bottom: 56.25%;
    }
    .iframe-video {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
    }
</style>
...
<div class="iframe-container">
    <iframe class="youtube-video iframe-video lazy" 
        data-src="https://www.youtube.com/embed/vx5-fQ67yDA"
        frameborder="0"
        allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen></iframe>
</div>
...
<script>
    document.addEventListener("DOMContentLoaded", function () {
        if ("IntersectionObserver" in window) {
            var iframes = document.querySelectorAll("iframe.lazy");
            var iframeObserver = new IntersectionObserver(function (entries, observer) {
                entries.forEach(function (entry) {
                    if (entry.isIntersecting && entry.target.src.length == 0) {                    
                        entry.target.src = entry.target.dataset.src;
                        iframeObserver.unobserve(entry.target);
                    }
                });
            });

            iframes.forEach(function (iframe) {
                iframeObserver.observe(iframe);
            });
        } else {
            var iframes = document.querySelector('iframe.lazy');

            for (var i = 0; i < iframes.length; i++) {
                if (lazyVids[i].getAttribute('data-src')) {
                    lazyVids[i].setAttribute('src', lazyVids[i].getAttribute('data-src'));
                }
            }
        }
    })
</script>
...

We adapted the JavaScript from Chris' blog to load the iframe on-demand. This snippet puts the iframe in a container, to make the iframe responsive to a 16:9 aspect ratio. When the iframe is visible to the user, the JavaScript grabs the data-src from the <iframe>, configures it as the src and loads the content of the iframe.

JavaScript Players

With JavaScript video players, the same principles as HTML videos apply: load the video when the video (player) becomes visible. Additionally, you want to asynchronously load their JavaScript libraries.

This codepen.io snippet uses the Intersection Observer API, the JavaScript API of the video player and async JavaScript to achieve lazy loading with the video player below.

Conclusion

Developers can use the Intersection Observer API to lazy load videos in order to optimize the critical rendering path.

There's a trade-off to consider when doing lazy loading videos.

  • The advantage? Lazy loading can decrease the page load time and can save the viewer some bandwidth.
  • The disadvantage? Lazy loading can increase the video start-up time. Akamai writes that viewers start abandoning a video if it takes longer than 2 seconds to load, and that an extra 5.8% drops off for every additional second of load time.
Akamai on the relationship between startup delay and abandonment rate w.r.t. 1) short videos vs long videos (left) and 2) connection types (right).

You should leverage lazy loading when it makes sense for your use-case. If you're building a video streaming website, you probably want to preload some videos. If you have a website/blog where video is secondary, you probably want to implement lazy loading. If you're doing livestreaming, it might not even make sense to preload data, because loaded data will be outdated when you hit play.

Resources