Write JavaScript like a pro. Javascript Icon

Follow the ultimate JavaScript roadmap.

Echo.js, simple JavaScript image lazy loading

I’m currently working on a project for Intel’s HTML5 Hub in which I require some image lazy-loading for an HTML5 showcase piece that’s high in image content. After a quick Google search for an existing lazy-load solution there was yet another mass of outdated scripts or jQuery plugins that were too time consuming to search through or modify for the project - so I ended up writing my own.

Echo.js is probably as simple as image lazy loading gets, it’s less than 1KB minified and is library agnostic (no jQuery/Zepto/other).

Table of contents

Lazy-loading works by only loading the assets needed when the elements ‘would’ be in view, which it’ll get from the server for you upon request, which is automated by simply changing the image src attribute. This is also an asynchronous process which also benefits us.

Using Echo.js

Using Echo is really easy, just include an original image to be used as a placeholder, for the demo I am using a simple AJAX .gif spinner as a background image with a transparent .gif placeholder so the user will always see something is happening, but you can use whatever you like.

Here’s the markup to specify the image source, which is literal so you’ll be able to specify the full file path (even the full https:// if you like) which makes it easier when working with directories.

<img src="img/blank.gif" alt="" data-echo="img/album-1.jpg">

Just drop the script into your page before the closing </body> tag and let it do its thing. For modern browsers I’ve used the DOMContentLoaded event incase you really need it in the <head>, which is a native ‘DOM Ready’, and a fallback to onload for IE7/8 if you need to go that far so all works nicely.

JavaScript

As always, I’ll talk through the script for those interested in the behind the scenes working. Here’s the full script:

window.echo = (function (window, document) {

  'use strict';

  /*
   * Constructor function
   */
  var Echo = function (elem) {
    this.elem = elem;
    this.render();
    this.listen();
  };

  /*
   * Images for echoing
   */
  var echoStore = [];

  /*
   * Element in viewport logic
   */
  var scrolledIntoView = function (element) {
    var coords = element.getBoundingClientRect();
    return ((coords.top >= 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || document.documentElement.clientHeight));
  };

  /*
   * Changing src attr logic
   */
  var echoSrc = function (img, callback) {
    img.src = img.getAttribute('data-echo');
    if (callback) {
      callback();
    }
  };

  /*
   * Remove loaded item from array
   */
  var removeEcho = function (element, index) {
    if (echoStore.indexOf(element) !== -1) {
      echoStore.splice(index, 1);
    }
  };

  /*
   * Echo the images and callbacks
   */
  var echoImages = function () {
    for (var i = 0; i < echoStore.length; i++) {
      var self = echoStore[i];
      if (scrolledIntoView(self)) {
        echoSrc(self, removeEcho(self, i));
      }
    }
  };

  /*
   * Prototypal setup
   */
  Echo.prototype = {
    init : function () {
      echoStore.push(this.elem);
    },
    render : function () {
      if (document.addEventListener) {
        document.addEventListener('DOMContentLoaded', echoImages, false);
      } else {
        window.onload = echoImages;
      }
    },
    listen : function () {
      window.onscroll = echoImages;
    }
  };

  /*
   * Initiate the plugin
   */
  var lazyImgs = document.querySelectorAll('img[data-echo]');
  for (var i = 0; i = 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || document.documentElement.clientHeight));
};

This uses a great addition to JavaScript, the .getBoundingClientRect() method which returns a text rectangle object which encloses a group of text rectangles, which are the border-boxes associated with that element, i.e. CSS box. The returned data describes the top, right, bottom and left in pixels. We can then make a smart comparison against the window.innerHeight or the document.documentElement.clientHeight, which gives you the visible area inside your browser on a cross-browser basis.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

Next up is a very simple function that switches the current image’s src attribute to the associated data-echo attribute once it’s needed:

var echoSrc = function (img, callback) {
  img.src = img.getAttribute('data-echo');
  if (callback) {
    callback();
  }
};

If a callback is present, it will run (I do pass in a callback here, but to prevent errors it’s good to simply if statement this stuff).

The next function I’ve setup to check if the current element exists in the array, and if it does, it removes it using the .splice() method on the current index to remove ‘itself’:

var removeEcho = function (element, index) {
  if (echoStore.indexOf(element) !== -1) {
    echoStore.splice(index, 1);
  }
};

The fundamental tie in for the script is listening for constant updates in the view based on our data store array. This function loops through our data store, and checks if the current element in the array is in view after initiating the scrolledIntoView function. If that proves to be true, then we call the echoSrc function, pass in the current element and also the current element’s index value, being i. This index value gets passed into the removeEcho function which in turn removes a copy of itself from the array. This means our array has become shorter and our JavaScript doesn’t have to work as hard or as long when looping through our leftover elements.

var echoImages = function () {
  for (var i = 0; i < echoStore.length; i++) {
    var self = echoStore[i];
    if (scrolledIntoView(self)) {
      echoSrc(self, removeEcho(self, i));
    }
  }
};

The OO piece of the script looks inside the prototype extension, which has a few functions inside. The first is the init() function, that simply pushes the current element into our data store array. The render() function checks to see if an addEventListener event exists, which will then invoke the echoImages function once the DOMContentLoaded event is fired. If it doesn’t exist, likely inside IE7/8, it’ll just run onload. The listen() function will just run the function again each time the window is scrolled, to poll and see if any elements come into view to work its magic some more.

Echo.prototype = {
  init : function () {
    echoStore.push(this.elem);
  },
  render : function () {
    if (document.addEventListener) {
      document.addEventListener('DOMContentLoaded', echoImages, false);
    } else {
      window.onload = echoImages;
    }
  },
  listen : function () {
    window.onscroll = echoImages;
  }
};

The final piece of the script is the beautiful API where you invoke a new Object on each item in a NodeList:

var lazyImgs = document.querySelectorAll('img[data-echo]');
for (var i = 0; i < lazyImgs.length; i++) {
  new Echo(lazyImgs[i]).init();
}

I chose to run a regular for loop on this, but if you’re routing for more modern JavaScript APIs you can of course do this which is much cleaner but unsupported in older IE (yes I can polyfill but the script is too small to warrant it):

[].forEach.call(document.querySelectorAll('img[data-echo]'), function (img) {
  new Echo(img).init();
}

Thank you for reading!

Learn JavaScript the right way.

The most complete guide to learning JavaScript ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover