Lazy Loading Images on the Web

lazy_load_header

Images on the web offer a bit of a conundrum. They are often what makes a web page feel vibrant and interesting, but they can also dramatically hurt web page performance by adding a significant amount of weight to a page.

On this site, we use a lot of images within our content. As the person who maintains this site, I do my best to optimize them, but in many cases the sum total of the image weight on a page is still significant (especially as we’ve come to rely on animated GIFs for illustrations). The thing is, much of this image weight sits well offscreen. As much as I’d like to believe that every visitor reads every word we write, it’s highly likely that many visitors are downloading images that they simply do not ever see.

The Page Weight Problem

This issue isn’t unique to this site. According to HTTPArchive, images now account for 63% of page weight. As developers, we work hard to optimize and minify our JavaScript code and CSS to make them as small as possible, but, by sheer volume, that can never have the impact of simply reducing image weight.

Consider that, as Tammy Everts notes, “images alone comprise more page weight (1310 KB) than an entire page in May 2012 (1059 KB), just three years ago.” It’s a problem for any visitor with a data cap – which is pretty much everyone, especially internationally. Images your visitors never see are costing them money.

And the page weight issue is only getting worse:

As of May [2015], the average web page surpassed the 2 MB mark. This is almost twice the size of the average page just three years ago. – source

So, what can we do?

Well, one option is to “lazy load” images. In essence, we use a little bit of JavaScript to determine which images are in (or near) the viewport and only download images that the user will likely see. It is a strategy that is not without its own flaws (some of which are covered in this article by the author of one lazy loading library). However, if your site relies on image-heavy content, it is a strategy worth considering – especially when targeting mobile devices.

In the remainder of this article, we’ll look at a number of different solutions for implementing lazy loading of images.

Sample Page

To assist us in reviewing the options for lazy loading images, I’ve built a simple example web page. The page is designed as a “Teen Titans Go” fan page that lists out a large number of the characters, with their images.

Teen Titans

I rebuilt the page using a variety of solutions to compare how they are implemented and how they work. The full code for all the variations can be found on GitHub.

Custom Solution

At its most basic implementation, building a custom solution for lazy loading images is not complicated. Here’s what we need to do:

  1. Build the HTML so that images are not automatically loaded (this is typically done by specifying the actual src in a data attribute);
  2. Watch changes to the viewport or scrolling to see which images have or may soon enter the viewport;
  3. Swap the data attribute and the src so that the image is loaded.

For the first item, you might be concerned that an img tag without a src is not valid HTML. Unfortunately, it appears that you cannot place the actual source in the src attribute while somehow preventing the browser from loading the image using JavaScript. You can place a “dummy” source image, however, such as a spacing or loading gif.

Let’s start by seeing how to build this in plain JavaScript.

Plain JavaScript

View this demo

The first thing is to make sure the images do not load by specifying the image source using a data-src attribute rather than placing it in the src. In our simple example, we are not going to worry about setting a dummy src or loading image.

<img data-src="images/Robin.jpg" alt="" />

I should note that you may want to add a width and height to your images to ensure that they take up the appropriate space. For our sample that isn’t completely necessary.

Now we need a way to test if an image is within the viewport. Thankfully, with support for getBoundingClientRect being nearly universal, there is a reliable method to do that. The function we are using is borrowed from a response on StackOverflow.

function isElementInViewport (el) {

    var rect = el.getBoundingClientRect();

    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
    );
}

To use this function, we’d pass an image or an image container and the function will return true if the passed element is within the viewport, or false if it is not.

Next, we need to get all of the images we want to lazy load and then test them against this function every time a scroll or change to the viewport occurs. Let’s look at the full code and then examine what it is doing.

//these handlers will be removed once the images have loaded
window.addEventListener("DOMContentLoaded", lazyLoadImages);
window.addEventListener("load", lazyLoadImages);
window.addEventListener("resize", lazyLoadImages);
window.addEventListener("scroll", lazyLoadImages);

function lazyLoadImages() {
  var images = document.querySelectorAll("#main-wrapper img[data-src]"),
      item;
  // load images that have entered the viewport
  [].forEach.call(images, function (item) {
    if (isElementInViewport(item)) {
      item.setAttribute("src",item.getAttribute("data-src"));
      item.removeAttribute("data-src")
    }
  })
  // if all the images are loaded, stop calling the handler
  if (images.length == 0) {
    window.removeEventListener("DOMContentLoaded", lazyLoadImages);
    window.removeEventListener("load", lazyLoadImages);
    window.removeEventListener("resize", lazyLoadImages);
    window.removeEventListener("scroll", lazyLoadImages);
  }
}

The first thing we do is ensure that we are watching for scrolls and changes to the viewport by listening on the DOMContentLoaded, load, resize and scroll events. Every time one of these events occurs, we call a method to check if any images have entered the viewport. (If you’re concerned about how often these events will be called, I discuss that issue in a later section.)

Looking at the lazyLoadImages method, we first get all the images that have not yet loaded. We do this by selecting only those that still have a data-src attribute. (As we’ll see in upcoming examples, there are a number of methods to do this, but, honestly, I have not tested which method is more performant. However, the performance implication of DOM node retrieval is negligible for the overwhelming majority of cases)

If the image has entered the viewport, we swap the value of the data-src attribute with the src attribute and remove the data-src attribute. Finally, if there are no images left unloaded, we simply remove the event listeners.

All in all, I’d say this was pretty simple, though it’s a very limited implementation. You could expand upon this to test for images sitting just outside the viewport so that there would be a potentially smaller visibility delay for the user – although this only really helps if the user manually scrolls.

jQuery

View this demo

If you use jQuery on your site, you can save a few lines of code. For instance, we can move turning the event handlers on and off into a single line of code. However, all in all, we save only a handful lines of code.

$(window).on('DOMContentLoaded load resize scroll', function () {;
  var images = $("#main-wrapper img[data-src]");
  // load images that have entered the viewport
  $(images).each(function (index) {
    if (isElementInViewport(this)) {
      $(this).attr("src",$(this).attr("data-src"));
            $(this).removeAttr("data-src");
    }
  })
  // if all the images are loaded, stop calling the handler
  if (images.length == 0) {
    $(window).off('DOMContentLoaded load resize scroll')
  }
})

// source: http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/7557433#7557433
function isElementInViewport (el) {
    var rect = el.getBoundingClientRect();

    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= $(window).height() &&
        rect.right <= $(window).width()
    );
}

If you go the build your own solution route, choosing straight JavaScript versus jQuery ends up being just a matter of personal preference.

Problems with These Solutions

Right now, we’ve just implemented extremely basic lazy loading functionality for images. A more robust solution would handle setting an offset, whereby elements just off screen are loaded as well as those on screen. Also, while the performance of our script may be suitable for a page with a relatively limited number of images (and, in this case, the images are not huge), its performance would likely degrade significantly for a more complex page with more images as the event handler is constantly being called and looping through a list of images.

Some additional features might be nice too, for instance having success and failure handlers for the loading process could prove useful. It’d also be nice to implement things like srcset and picture for responsive images. Lastly, I might want to add support for a loading images in case the image file isn’t fully downloaded when it enters the viewport. I might even, perhaps, want some sort of animation or transition when images appear.

The good news is that there are a lot of pre-built libraries for handling lazy loading of images available that do many of these things. Let’s look at some of them.

Libraries

For the most part, as you look through the library variations below, you may notice that they all function very similarly. For our sample, the implementation looks surprisingly similar across the board. Choosing one comes down to whether you want to require jQuery (most do) and what kind of options you need, as some offer far more configuration than others.

LazyLoad

View this demo

One of the first lazy loading image libraries was the Lazy Load Plugin for jQuery. It inspired a number of additional libraries including LazyLoad.

One difference between LazyLoad and the other solutions is that the images use data-original for the source rather than data-src.

<img data-original="images/GreenLantern.jpg" alt="" width="374" height="260" />

Once our images are set up, lazy loading them is incredibly simple. Just include the JavaScript file (obviously) and initialize LazyLoad. In our sample code below, we’ve added a threshold of 50 pixels, meaning that items just off-screen will also be loaded. We’ve also added a success handler.

var myLazyLoad = new LazyLoad({
  threshold: 50,
  callback_load: function(e) {
    console.log($(e).attr("data-original") + " loaded" );
  }
});

The image below shows scrolling through the mobile version of the page loading one image at a time. In the console, each image indicates that it has successfully loaded.

LazyLoad_opt

LazyLoad supports a number of additional options, which we won’t cover here.

bLazy.js

View this demo

bLazy (or [Be]Lazy) is a relatively recent library that aims to offer a number of key features while remaining small and lightweight. One significant difference of bLazy is that it does not require jQuery.

To identify which images are to be lazy loaded, by default bLazy requires you to add a CSS class onto each. In this example, I am using the default CSS class, but the selector can be customized.

<img data-src="images/Batgirl.jpg" alt="" width="374" height="260" class="b-lazy" />

After that, you simply include and initialize the script. Below I have done that while also setting an offset and a success and error handler.

var bLazy = new Blazy({
    offset: 50,
    success: function(e){
        console.log($(e).attr("src") + " loaded" );
  },
    error: function(ele, msg){
        console.log(msg)
  }
});

Below you can see multiple images loading at a time as the page is scrolled.

blazy_opt

bLazy supports a number of options including the ability to serve smaller or larger images based upon screen size. Check the documentation for more details.

Unveil

View this demo

Unveil is another script inspired by jQuery_lazyload, so, it does require jQuery. It is very lightweight, however, being less than 1k.

One nice thing about Unveil is that it does not require any special markup on your images beyond the data-src attribute.

<img data-src="images/WonderTwins.jpg" alt="" />

This is because we specify the images that will be supplied to Unveil via a jQuery selector. Simply load the script and apply Unveil to our selected images on document ready.

$(function () {
  $("#main-wrapper img").unveil(50, function() {
    $(this).load(function() {
      console.log(this.src + " loaded");
    });
  });
})

As with the prior examples, we’ve also initialized Unveil with an offset of 50 pixels and a success handler.

Unveil is more lightweight than the other libraries and, because of this, doesn’t offer quite as many options as the others discussed. Check the documentation for additional usage options.

Lazy Load XT

View this demo

Lazy Load XT probably offers the most features of any of the libraries discussed here. In their Smashing Magazine article, the authors described their aim to fix many of the deficiencies in the other available libraries.

Lazy Load XT splits itself into two main libraries – the first has the core features and the second adds some of the more advanced options. It also offers additional scripts as plugins and CSS files for effects. For our sample, we only need the basic script file.

<script src="js/jquery.lazyloadxt.js"></script>

Just as with Unveil previously, our images only need a data-src attribute. There is no need for any special classes or additional data attributes.

<img data-src="images/BeastBoy.jpg" alt="" />

Now, all we need to do is initialize Lazy Load XT. In the below code, we are also adding success and error handlers by listening for specific events that LazyLoad emits.

$(window).lazyLoadXT();
$(window).on("lazyload", function(e){
    console.log(e.target.src + " loaded");
});
$(window).on("lazyerror", function(e){
    console.log("Error loading " + e.target.src);
});

Lazy Load XT supports a lot of options and events. I should note, however, that I had some difficulty getting certain options to work easily. While Lazy Load XT has a lot of examples and documentation, it seems (to me anyway) to be missing the simple examples, such as using some of the basic events and configuration options. I also, unfortunately, had some difficulty even getting the basic effects to work (in theory, this was simply a matter of including the correct CSS).

On a more positive note, Lazy Load XT has extensive support for responsive images. It even offers support for lazy loading videos and iframes, making it by far the most comprehensive lazy loading library I came across. Someone has even created a WordPress plugin to lazy load images included within WordPress posts.

Telerik Platform – Responsive Images Service

View this demo

Let’s look at one more example. Within the Telerik Platform‘s Backend Services offering is a responsive images service. It offers:

  • The ability to specify image dimensions for individual images;
  • Automatic resizing of images based on the target container dimensions.

The main benefit for our purposes is that using this service the images are automatically responsive. For example, we can place a huge image in Backend Services but the service will automatically serve the appropriate image based on the device’s screen size and pixel density — no need to use srcset/picture, or to upload ten versions of the images.

For a full overview of the responsive images offering, check out this article by Hristo Borisov.

In addition, Backend Services offer the ability to store and serve files from the CDN. While this isn’t necessary to utilize the responsive images features, it is definitely a plus.

Platform_files

After uploading the files, we need to right click the file name (i.e. the Batgirl.jpg link in the screenshot above) to get its URL (see the documentation if you need additional details).

In the HTML, we need to swap all the images for the CDN-hosted versions within the data-src attribute. We also need to add a data-responsive attribute to each image. This enables the automatic image replacement – more on that in a moment. For instance, here’s the image tag for Raven (aka my favorite Teen Titan), where the URL follows an /api-key/image-id format.

<img data-src="https://bs2.cdn.telerik.com/v1/h5tc7Cws1Qi9oluI/de916422-6e1f-11e5-a7c8-356526b6da24" alt="" data-responsive />

We are going to use the JavaScript SDK that Backend Services provides to automatically apply responsive images. So first, let’s include the script.

<script src="https://bs-static.cdn.telerik.com/1.5.6/everlive.all.min.js"></script>

Next we need to initialize the service. There are a number of additional options that you can specify here, but let’s stick with the defaults.

var el = new Everlive({
    apiKey: "your-api-key",
    scheme: "https"
});

Since the service is not dependent on jQuery, let’s go ahead and leverage the plain JavaScript version we built earlier. We also need to include the isElementInViewport function shown earlier. After that, it’s just some minor changes to the plain JavaScript code we wrote previously.

window.addEventListener("DOMContentLoaded", lazyLoadImages);
window.addEventListener("load", lazyLoadImages);
window.addEventListener("resize", lazyLoadImages);
window.addEventListener("scroll", lazyLoadImages);

function lazyLoadImages() {
  // select only images that do not yet have a src attribute added
  var images = document.querySelectorAll("#main-wrapper img:not([src])"),
      item;

  // load images that have entered the viewport
  [].forEach.call(images, function (item) {
    if (isElementInViewport(item)) {
            el.helpers.html.process(item, {}, successHandler, errorHandler);
    }
  })
  // if all the images are loaded, stop calling the handler
  if (images.length == 0) {
    window.removeEventListener("DOMContentLoaded", lazyLoadImages);
    window.removeEventListener("load", lazyLoadImages);
    window.removeEventListener("resize", lazyLoadImages);
    window.removeEventListener("scroll", lazyLoadImages);
  }
}

function successHandler (e) {
    console.log("success");
}
function errorHandler (e) {
    console.log(e);
}

There are a couple of small but important changes to note here. First, we have changed our selector to select any images that do not have a src attribute. The automatic responsive image loading does not like us removing the data-src attribute, but, using this method, we can still select only those images that have not yet been loaded.

Second, if the image is within the viewport, we call el.helpers.html.process() to have the service automatically process them. We’re not passing any options at this time, although we did set success and failure handlers. Since we are loading from an external CDN, it would probably be worth expanding this to support an offset in the future.

There’s many more options available in the responsive images service. If you’re interested in utilizing it, I suggest reading through the documentation to learn more about what is available.

Go Further

Hopefully, by now, you have a good overview of the benefits of lazy loading images and some of the techniques available to you for achieving it. Our samples were intentionally simple, but the options exist to take this to the next level including handling things like responsive images, video, iframes and widgets, depending on the needs of your site.

The important thing to keep in mind here is the cost/benefit to your users. Does the potential for a slight delay in image loading (and, perhaps, a decrease in “scan-ability” of the content) offset the problem of image bloat? Or does your content rely heavily on large imagery that the user may never see, but which may be costing them in terms of loading time or data overages? Is it possibly worth implementing this strategy but targeting specifically mobile, where data can be slow and costly, while ignoring desktops, where WiFi would pretty much be guaranteed?

Share your thoughts in the comments.

Header image courtesy of Andy Rennie

How to Lead, Innovate and Win in the Age of Mobile Apps

Comments

  • Pingback: Dew Drop – October 19, 2015 (#2114) | Morning Dew()

  • Great article. Makes me wonder if this is possible with React?

  • pawan gangwani

    Can we implement this in angular. If yes, please pointers?

  • Andrew Ritter

    yes, just include the JS or jQuery script as you need it or load the library (like LazyLoad) you want use alongside the framework you are also using

  • I think a better approach would be to loop through all of the images you want to lazy load, calculate the offset from the top of the browser and store that in an array. As you scroll down the page you can just loop over the array to identify if it is time to load an image and if so remove that value from the array.

    This way you don’t need to query the DOM and do all sorts of calculations on every scroll event. Debouncing the scroll event so it doesn’t fire as often can also help performance. See http://davidwalsh.name/javascript-debounce-function

    • my eyes are hungry

      Hmm wouldn’t that require you know the exact position it will appear once the dom finishes loading? Non loaded images will have no height and then pop in and push the page down. You could set it, but then that’s not responsive friendly.

    • Sounds like a neat idea on the outset but if you’re storing values in the array, you’ll have to keep updating the array values whenever they need to be updated, like when the viewport is resized after the page has loaded or when elements are injected into the DOM. This essentially leads you right back to the having to re-calculate all elements constantly. Although, I’m not too sure how to avoid the original issue.

  • Pingback: To read tech | Pearltrees()

  • Davemanroth

    Great article, Brian, thanks for posting this1

  • Jerry Oswald

    I create most images myself as vectors and just use them as is. I don’t use many libraries and none if I can swing it. If you use SVG at least for icons, you are one step closer to lifting some weight off the page. The raw SVG can even be made smaller by running them through an optimizing program and compressing them. SVG browser support like most things web, are supported at least for some of the basics like transform, translate, scale and viewbox.

  • urbgimtam

    Great article and roundup. I guess that, if you pair the images with other strategies like progressive-jpgs, the performance gain is even greater, and the user is even less penalized.

  • Good stuff Brian

  • Pingback: Weekly Designer and Developer News #3()

  • Pingback: To read - chwy | Pearltrees()

  • Pingback: Today’s Readings | Aaron T. Grogg()

  • Pingback: Article Roundup - Hoverboard Studios()

  • Pingback: 28-10-2015 - Links - Magnus Udbjørg()

  • I would like to present you another library (a new way to resolve the problem actually). It’s available https://github.com/ivopetkov/responsively-lazy and uses a srcset tricks. It does not modify the src attribute, so the HTML is valid and SEO-friendly. You can read more at http://ivopetkov.com/b/lazy-load-responsive-images/

  • Pingback: SEO for Image Based Sites | Applicacious()

  • Omotayo Iginla

    awesome article…great

  • Hey nice article. Don’t mean to link drop, but just wanted to mention that there is a library called image-element that I contribute to that lazy loads images with srcsets and has error and success states in addition element-listener-js that provides a way to detect when an element is inside the viewport and supports offsets. Using things like this together can help tie up a lot of loose ends you mentioning at the bottom of your article. The packages are also supported by npm and bower also so double win! Happy coding!

  • Pingback: Web Design and Maslow’s Hierarchy of Needs - Alex Devero Blog()