Lazy Loading Images with ES5

Browsers typically load all of the images on the page as soon as possible. This is called eager loading. Sometimes, it's best if we wait to load images until we need them. This is called lazy loading. Lazy loading is a great technique for speeding up page load times and decreasing page sizes.

How Do You Set Images to Lazy Load?

I wanted to make images on my blog load lazily. I chose to use the Intersection Observer API to determine when images are almost on-screen. When this happened, I have some JavaScript that populates the almost-visible <img> tag's src attribute with the correct value. From there, the browser takes over and loads the image.

The Intersection Observer API is, as of October 2019, supported by a wide range of browsers:

I decided to write my lazy-loading code myself so I could make sure I fully understood how it works. I also decided to use ES5 to make sure that the code could run on just about any browser. I don't currently use Babel on my blog, and I don't feel like setting it up, so ES5 it is.

The code checks if the image is close to being displayed. If it is, then it copies the content of the data-src, data-srcset, and data-sizes attributes to the src, srcset, and sizes attributes, respectively. All browsers load images as soon as they have a src or srcset attribute.

The Code

Here's the code I wrote:

// lazy-load-images.js

function lazyLoadImagesCallback(entries, observer) {
  for (var i = 0; i < entries.length; i++) {
    var entry = entries[i];
    if (entry.isIntersecting) {
      var img = entry.target;
      copyImgAttributesFromData(img);
      observer.unobserve(img);
    }
  }
}

function observeLazyLoadImages() {
  var options = {
    // If the image is within half a viewport of the screen, load it
    rootMargin: "50%"
  };

  var observer = new IntersectionObserver(lazyLoadImagesCallback, options);

  var imgs = document.querySelectorAll("img");
  for (var i = 0; i < imgs.length; i++) {
    var img = imgs[i];
    observer.observe(img);
  }
}

function copyImgAttributesFromData(img) {
  var src = img.getAttribute("data-src");
  var srcset = img.getAttribute("data-srcset");
  var sizes = img.getAttribute("data-sizes");
  if (src) {
    img.setAttribute("src", src);
  }
  if (srcset) {
    img.setAttribute("srcset", srcset);
  }
  if (sizes) {
    img.setAttribute("sizes", sizes);
  }
}

function loadAllImages() {
  var imgs = document.querySelectorAll("img");
  for (var i = 0; i < imgs.length; i++) {
    var img = imgs[i];
    copyImgAttributesFromData(img);
  }
}

function loadImages() {
  if ("IntersectionObserver" in window) {
    observeLazyLoadImages();
  } else {
    loadAllImages();
  }
}

if (document.readyState !== "loading") {
  loadImages();
} else {
  document.addEventListener("DOMContentLoaded", function() {
    loadImages();
  });
}

Fallback Behavior

It has one fallback: if the browser doesn't support the Intersection Observer API (i.e. the browser is Internet Explorer), then it just goes ahead and loads all the images.

Assumptions

This script assumes that you're using the data-src, data-srcset, and data-sizes attributes on your <img> tags. If you're not, it doesn't do anything.

<!-- This image will be lazy loaded -->
<img data-src="puppy.jpg">
<!-- This image will NOT be lazy loaded -->
<img src="puppy.jpg">

Summary

Lazy loading images saves bandwidth, and it's not too hard to implement.

Photo by Vita Vilcina