A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.md 27KB

title: Lazy Loading Images on the Web url: http://developer.telerik.com/featured/lazy-loading-images-on-the-web/ hash_url: 911d8d2cce

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();&#13;

return (&#13;
    rect.top &gt;= 0 &amp;&amp;&#13;
    rect.left &gt;= 0 &amp;&amp;&#13;
    rect.bottom &lt;= (window.innerHeight || document.documentElement.clientHeight) &amp;&amp; /*or $(window).height() */&#13;
    rect.right &lt;= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */&#13;
);&#13;

}

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;&#13;

// load images that have entered the viewport [].forEach.call(images, function (item) {

if (isElementInViewport(item)) {&#13;
  item.setAttribute("src",item.getAttribute("data-src"));&#13;
  item.removeAttribute("data-src")&#13;
}&#13;

}) // if all the images are loaded, stop calling the handler if (images.length == 0) {

window.removeEventListener("DOMContentLoaded", lazyLoadImages);&#13;
window.removeEventListener("load", lazyLoadImages);&#13;
window.removeEventListener("resize", lazyLoadImages);&#13;
window.removeEventListener("scroll", lazyLoadImages);&#13;

} }

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)) {&#13;
  $(this).attr("src",$(this).attr("data-src"));&#13;
        $(this).removeAttr("data-src");&#13;
}&#13;

}) // if all the images are loaded, stop calling the handler if (images.length == 0) {

$(window).off('DOMContentLoaded load resize scroll')&#13;

} }) // 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();&#13;

return (&#13;
    rect.top &gt;= 0 &amp;&amp;&#13;
    rect.left &gt;= 0 &amp;&amp;&#13;
    rect.bottom &lt;= $(window).height() &amp;&amp;&#13;
    rect.right &lt;= $(window).width()&#13;
);&#13;

}

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" );&#13;

} });

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,&#13;
success: function(e){&#13;
    console.log($(e).attr("src") + " loaded" );&#13;

},

error: function(ele, msg){&#13;
    console.log(msg)&#13;

} });

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() {&#13;
  console.log(this.src + " loaded");&#13;
});&#13;

}); })

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");&#13;

}); $(window).on(“lazyerror”, function(e){

console.log("Error loading " + e.target.src);&#13;

});

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",&#13;
scheme: "https"&#13;

});

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;&#13;

// load images that have entered the viewport [].forEach.call(images, function (item) {

if (isElementInViewport(item)) {&#13;
        el.helpers.html.process(item, {}, successHandler, errorHandler);&#13;
}&#13;

}) // if all the images are loaded, stop calling the handler if (images.length == 0) {

window.removeEventListener("DOMContentLoaded", lazyLoadImages);&#13;
window.removeEventListener("load", lazyLoadImages);&#13;
window.removeEventListener("resize", lazyLoadImages);&#13;
window.removeEventListener("scroll", lazyLoadImages);&#13;

} } function successHandler (e) {

console.log("success");&#13;

} function errorHandler (e) {

console.log(e);&#13;

}

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