How to Build Simple and Powerful Lazyload JavaScript Plugin

Reading Time: 12 minutes

Have you ever wanted to use lazyload plugin to speed up your website? Who wouldn’t. The problem is that most of lazyload plugins require jQuery. Sure, there are some exception. However, you need advanced knowledge of JavaScript if you want to understand the code. If you don’t have that, forget about customizing the plugin. You have to use it as it is. Well, not anymore! Today, you will learn how to build your own lazyload plugin. Take control and improve your JavaScript skills!

Note: There is nothing wrong with jQuery. jQuery, and other libraries as well, can save you a lot of time. However, if you want to get done just one task, using whole library is not necessary. Actually, it can be a waste of your resources. Think about it. Even the slim version of jQuery has more than 60kb! Is this really necessary for one small task such as lazyloading images? I don’t think so. Write your own lazyload plugin and use these kilobytes in a smarter way!

Live demo on CodePen.

Source code on GitHub.

One thing to think about

There is one thing we have to think about before we begin. What if JavaScript is disabled or not available. I know that this is very unlikely, but it can happen. Someone can visit your website with device or browser not supporting or allowing JavaScript. In that case, there will be no content. It doesn’t matter what technology people want to use. We should make the content accessible under majority of conditions. This is what progressive enhancement and good work is about.

Fortunately, there is a quick fix. First, we will add a duplicate every image tag in the markup and wrap it inside noscript tag. Second, we will add no-js class to html and lazy class to images for lazyload plugin (outside noscript). Then, when we initiate lazyload plugin, it will remove the no-js class. Finally, with CSS, we will combine these two classes to hide images. So, if JavaScript is not available, html element will have no-js class. And, images with class lazy inside it will be hidden.

As a result, user will be able to see only “fallback” images we added that are inside noscript tag. The upside of this approach is its simplicity. The downside is that it requires modification of HTML and CSS. Still, it is better than showing nothing at all. Would you agree?

HTML

Yes, this is a tutorial about building lazyload JavaScript plugin. So, why do we need to talk about HTML? Well, we don’t have to. This part, and the part about CSS, are just for demonstration. You are free to skip these two parts and move to the JavaScript part. The only thing you should know, related to HTML, is our minimal markup. It doesn’t matter how powerful lazyload plugin we build, it still can’t read our minds. At least not at this moment. Maybe we will get to it in the future.

It is for this reason that we have to establish some requirements for our lazyload plugin. We need to explicitly say what attributes are necessary. We will use data attribute. So, you can change the names of these attributes as you wish. For now, the minimum we will need is either src or srcset attribute. If any one of these two attributes is present, our lazyload plugin will be able to do the job. And in order to keep things as simple as possible, let’s use data-src and data-srcset attributes.

As I mentioned in the intro, we will also use images inside noscript tag as a fallback. This fallback will use the same values we used for data-src and data-srcset. However, we will use implement them through regular src and srcset attributes, logically. One last thing. You will see some divs with classes like container-fluid, etc. I used Bootstrap framework for grid, nothing more. So, this framework is NOT required for our lazyload plugin.

Note: the 2x version of the image in data-srcset or srcset attributes is for devices with device pixel ratio of 2. In other words, high-density displays such as retina screens.

HTML:

<div class="container-fluid">
 <div class="row">
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 1" data-src="https://source.unsplash.com/ozwiCDVCeiw/450x450" data-srcset="https://source.unsplash.com/ozwiCDVCeiw/450x450 1x, https://source.unsplash.com/ozwiCDVCeiw/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/ozwiCDVCeiw/450x450" alt="Example photo 1" srcset="https://source.unsplash.com/ozwiCDVCeiw/450x450 1x, https://source.unsplash.com/ozwiCDVCeiw/900x900 2x" />
   </noscript>
  </div>

  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 2" data-src="https://source.unsplash.com/SoC1ex6sI4w/450x450" data-srcset="https://source.unsplash.com/SoC1ex6sI4w/450x450 1x, https://source.unsplash.com/SoC1ex6sI4w/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/SoC1ex6sI4w/450x450" alt="Example photo 2" srcset="https://source.unsplash.com/SoC1ex6sI4w/450x450 1x, https://source.unsplash.com/SoC1ex6sI4w/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 3" data-src="https://source.unsplash.com/oXo6IvDnkqc/450x450" data-srcset="https://source.unsplash.com/oXo6IvDnkqc/450x450 1x, https://source.unsplash.com/oXo6IvDnkqc/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/oXo6IvDnkqc/450x450" alt="Example photo 3" srcset="https://source.unsplash.com/oXo6IvDnkqc/450x450 1x, https://source.unsplash.com/oXo6IvDnkqc/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 4" data-src="https://source.unsplash.com/gjLE6S4omY0/450x450" data-srcset="https://source.unsplash.com/gjLE6S4omY0/450x450 1x, https://source.unsplash.com/gjLE6S4omY0/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/gjLE6S4omY0/450x450" alt="Example photo 4" srcset="https://source.unsplash.com/gjLE6S4omY0/450x450 1x, https://source.unsplash.com/gjLE6S4omY0/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 5" data-src="https://source.unsplash.com/KeUKM5N-e_g/450x450" data-srcset="https://source.unsplash.com/KeUKM5N-e_g/450x450 1x, https://source.unsplash.com/KeUKM5N-e_g/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/KeUKM5N-e_g/450x450" alt="Example photo 5" srcset="https://source.unsplash.com/KeUKM5N-e_g/450x450 1x, https://source.unsplash.com/KeUKM5N-e_g/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 6" data-src="https://source.unsplash.com/gjLE6S4omY0/450x450" data-srcset="https://source.unsplash.com/gjLE6S4omY0/450x450 1x, https://source.unsplash.com/gjLE6S4omY0/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/gjLE6S4omY0/450x450" alt="Example photo 6" srcset="https://source.unsplash.com/gjLE6S4omY0/450x450 1x, https://source.unsplash.com/gjLE6S4omY0/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 7" data-src="https://source.unsplash.com/7eKCe28OG6E/450x450" data-srcset="https://source.unsplash.com/7eKCe28OG6E/450x450 1x, https://source.unsplash.com/7eKCe28OG6E/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/7eKCe28OG6E/450x450" alt="Example photo 7" srcset="https://source.unsplash.com/7eKCe28OG6E/450x450 1x, https://source.unsplash.com/7eKCe28OG6E/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 8" data-src="https://source.unsplash.com/0Pz4h4_O3PU/450x450" data-srcset="https://source.unsplash.com/0Pz4h4_O3PU/450x450 1x, https://source.unsplash.com/0Pz4h4_O3PU/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/0Pz4h4_O3PU/450x450" alt="Example photo 8" srcset="https://source.unsplash.com/0Pz4h4_O3PU/450x450 1x, https://source.unsplash.com/0Pz4h4_O3PU/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 9" data-src="https://source.unsplash.com/cFplR9ZGnAk/450x450" data-srcset="https://source.unsplash.com/cFplR9ZGnAk/450x450 1x, https://source.unsplash.com/cFplR9ZGnAk/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/KeUKM5N-e_g/450x450" alt="Example photo 9" srcset="https://source.unsplash.com/cFplR9ZGnAk/450x450 1x, https://source.unsplash.com/cFplR9ZGnAk/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 10" data-src="https://source.unsplash.com/UO02gAW3c0c/450x450" data-srcset="https://source.unsplash.com/UO02gAW3c0c/450x450 1x, https://source.unsplash.com/UO02gAW3c0c/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/UO02gAW3c0c/450x450" alt="Example photo 10" srcset="https://source.unsplash.com/UO02gAW3c0c/450x450 1x, https://source.unsplash.com/UO02gAW3c0c/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 11" data-src="https://source.unsplash.com/3FjIywswHSk/450x450" data-srcset="https://source.unsplash.com/3FjIywswHSk/450x450 1x, https://source.unsplash.com/3FjIywswHSk/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/3FjIywswHSk/450x450" alt="Example photo 11" srcset="https://source.unsplash.com/3FjIywswHSk/450x450 1x, https://source.unsplash.com/3FjIywswHSk/900x900 2x" />
   </noscript>
  </div>
 
  <div class="col-md-2 col-lg-3">
   <img alt="Example photo 12" data-src="https://source.unsplash.com/z_L0sZoxlCk/450x450" data-srcset="https://source.unsplash.com/z_L0sZoxlCk/450x450 1x, https://source.unsplash.com/z_L0sZoxlCk/900x900 2x" class="lazy" />
 
   <noscript>
    <img src="https://source.unsplash.com/z_L0sZoxlCk/450x450" alt="Example photo 12" srcset="https://source.unsplash.com/z_L0sZoxlCk/450x450 1x, https://source.unsplash.com/z_L0sZoxlCk/900x900 2x" />
   </noscript>
  </div>
 </div>
</div>

CSS

Well, there is not so much to talk about. In terms of CSS, we will need to do only three things. First, we need to add styles for hiding images if JavaScript is not supported. Setting display property to ”none” will do the job. Second, we will add a small “fix” to hide images without src attribute. Otherwise, browsers would render these images as broken. We will use visibility and set it to “hidden” to hide these images.

Finally, it can happen that the image is bigger than the container, its parent. This could cause the image to overlap and break the layout. In order to make sure this never happens, we will use max-width and set it to “100%”. As a result, images can be as big as the container, but not bigger. At first, I wanted to apply these CSS styles via lazyload plugin (JavaScript). However, I decided to not to. You guessed it! These styles would not work without JavaScript (images inside noscript tags).

CSS:

/* Hide lazyload images if JavaScript is not supported */
.no-js .lazy {
 display: none;
}

/* Avoid empty images to appear as broken */
img:not([src]):not([srcset]) {
 visibility: hidden;
}

/* Fix for images to never exceed the width of the container */
img {
 max-width: 100%;
}

JavaScript

And, we are getting to the main part of this tutorial! Now, we will finally build our lazyload plugin. The whole lazyload plugin will consist of three main parts. The first one will help us test whether the image is in viewport, or visible. The second part will be a custom fade in effect. We will manipulate with the opacity of the image to show it. This will be better than “blinking” the image. The last part will take all images and set src and srcset attributes to the content of data attributes.

This all will be wrapped inside arrow function and assigned to lazyloadVanilla constant. And, this will be wrapped inside self-invoking anonymous arrow function. One more thing. In the end we will add a number of eventListeners and a short script to test for JavaScript support (html and no-js class). We will use event listeners to watch for DOMContentLoaded, load, resize and scroll events. All these listeners will use lazyloadVanillaLoader() function as listener (initiate this function).

In other words, when the content of the DOM is loaded or the window is resized or scrolled, it will initiate lazyloadVanillaLoader() function. Finally, on the last line, we will return lazyloadVanilla() to initiate our lazyload plugin. So, our starting structure will be following:

JavaScript:

(() => {
 const lazyloadVanilla = () => {}

 // Test if JavaScript is available and allowed
 if (document.querySelector('.no-js') !== null) {
  document.querySelector('.no-js').classList.remove('no-js');
 }

 // Add event listeners to images
 window.addEventListener('DOMContentLoaded', lazyloadVanillaLoader);

 window.addEventListener('load', lazyloadVanillaLoader);

 window.addEventListener('resize', lazyloadVanillaLoader);

 window.addEventListener('scroll', lazyloadVanillaLoader);

 // Initiate lazyloadVanilla plugin
 return lazyloadVanilla();
})();

Testing the viewport

Let’s start with the script for testing if image is in viewport. We will create function called isImageInViewport. ­This function will take one parameter, the image. It will detect the size of this image and also its position relative to the viewport. We will do this by using getBoundingClientRect() mehod. Then, we will compare the size and position of the image with innerWidth and innerHeight of window. And, we will return either true (is in viewport) or false.

JavaScript:

const isImageInViewport = (img) => {
 const rect = img.getBoundingClientRect();

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

Custom fade in effect

The second part of our lazyload plugin is making images fade in smoothly. To do this, we will create fadeInCustom function. This function will also take one parameter, the image. Inside this function, we will create variable (let) called elementOpacity to store initial opacity. This opacity will be “0.1”. Next, we will take the element provided as parameter and set its display CSS property to “block”. Then, we will create variable timer and assign setInterval() method to it.

Inside this interval will be if statement to check if the opacity of the element is bigger than “1”. If so, it will clear, or reset, the interval. Otherwise, we will set the opacity of the element to the value of elementOpacity variable. We will do the same with filter property for older browsers. Then, we will increase the value of elementOpacity variable. Finally, we will repeat this interval every 15ms until the opacity is 1 and image is completely visible.

JavaScript:

// Create custom fading effect for showing images
const fadeInCustom = (element) => {
 let elementOpacity = 0.1;// initial opacity

 element.style.display = 'block';

 const timer = setInterval(() => {
  if (elementOpacity >= 1){
   clearInterval(timer);
  }

  element.style.opacity = elementOpacity;

  element.style.filter = 'alpha(opacity=' + elementOpacity * 100 + ")";

  elementOpacity += elementOpacity * 0.1;
 }, 15);
};

The core

It’s time to take care about the core of our lazyload plugin. We will create lazyloadVanillaLoader function. Unlike the previous, this function will take no parameters. Inside this function, we will collect all images with data-src attribute and store them inside lazyImagesArray variable. Then, we will use forEach() method to loop through the list of images. You can also use for loop if you want. Anyway, for each image, we will do a number of things.

The first one is testing if image is in viewport. So, we will call isImageInViewport() function and pass individual images as parameter. If it is, will then test if the image has data-src attribute. If it does, we will take its value and set it as a value of src attribute. Then, we will remove the data-src attribute because we will use it to do a little test. We will do the same with data-srcset attribute. We can also create data-loaded attribute and set it to “true”.

Finally, we will use fadeInCustom() function with “image” as parameter to smoothly fade in the image. Now it is time to do that little test I mentioned in previous paragraph. We will again query the DOM and look for all images with data-src or data-srcset attribute. What’s next? Do you remember those event listeners we attached to the window object in the beginning? When all images are loaded, we don’t need them anymore. Therefore, we can remove these listeners.

JavaScript:

// lazyloadVanilla function
const lazyloadVanillaLoader = () => {
 const lazyImagesList = document.querySelectorAll('img[data-src]');
 
 lazyImagesList.forEach((image) => {
  if (isImageInViewport(image)) {
   if (image.getAttribute('data-src') !== null) {
    image.setAttribute('src', image.getAttribute('data-src'));

    image.removeAttribute('data-src');
   }

   if (image.getAttribute('data-srcset') !== null) {
    image.setAttribute('srcset', image.getAttribute('data-srcset'));

    image.removeAttribute('data-srcset');
   }

   image.setAttribute('data-loaded', true);

   fadeInCustom(image);
  }
 });

 // Remove event listeners if all images are loaded
 if (document.querySelectorAll('img[data-src]').length === 0 && document.querySelectorAll('img[data-srcset]')) {
  window.removeEventListener('DOMContentLoaded', lazyloadVanilla);

  window.removeEventListener('load', lazyloadVanillaLoader);

  window.removeEventListener('resize', lazyloadVanillaLoader);

  window.removeEventListener('scroll', lazyloadVanillaLoader);
 }
};

Putting the pieces together

This is it! We now have all the parts necessary to get our lazyload plugin up and running. Let’s now put all the pieces together so you can see it all at once. By the way, great work! :+1:

JavaScript:

(() => {
 const lazyloadVanilla = () => {
  // Test if image is in the viewport
  const isImageInViewport = (img) => {
   const rect = img.getBoundingClientRect();

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

  // Create custom fading effect for showing images
  const fadeInCustom = (element) => {
   let elementOpacity = 0.1;// initial opacity

   element.style.display = 'block';

   const timer = setInterval(() => {
    if (elementOpacity >= 1){
     clearInterval(timer);
    }

    element.style.opacity = elementOpacity;

    element.style.filter = 'alpha(opacity=' + elementOpacity * 100 + ")";

    elementOpacity += elementOpacity * 0.1;
   }, 15);
  };

  // lazyloadVanilla function
  const lazyloadVanillaLoader = () => {
   const lazyImagesList = document.querySelectorAll('img[data-src]');
 
   lazyImagesList.forEach((image) => {
    if (isImageInViewport(image)) {
     if (image.getAttribute('data-src') !== null) {
      image.setAttribute('src', image.getAttribute('data-src'));

      image.removeAttribute('data-src');
     }

     if (image.getAttribute('data-srcset') !== null) {
      image.setAttribute('srcset', image.getAttribute('data-srcset'));

      image.removeAttribute('data-srcset');
     }

     image.setAttribute('data-loaded', true);

     fadeInCustom(image);
    }
   });

   // Remove event listeners if all images are loaded
   if (document.querySelectorAll('img[data-src]').length === 0 && document.querySelectorAll('img[data-srcset]')) {
    window.removeEventListener('DOMContentLoaded', lazyloadVanilla);

    window.removeEventListener('load', lazyloadVanillaLoader);

    window.removeEventListener('resize', lazyloadVanillaLoader);

    window.removeEventListener('scroll', lazyloadVanillaLoader);
   }
  };

  // Add event listeners to images
  window.addEventListener('DOMContentLoaded', lazyloadVanillaLoader);

  window.addEventListener('load', lazyloadVanillaLoader);

  window.addEventListener('resize', lazyloadVanillaLoader);

  window.addEventListener('scroll', lazyloadVanillaLoader);
 }
 
 // Test if JavaScript is available and allowed
 if (document.querySelector('.no-js') !== null) {
  document.querySelector('.no-js').classList.remove('no-js');
 }
 
 // Initiate lazyloadVanilla plugin
 return lazyloadVanilla();
})();

Closing thoughts on building lazyload plugin

This is the end of this tutorial ladies and gentlemen. You’ve built your own lazyload plugin using only pure JavaScript. In addition, you also trained ES6 JavaScript syntax. I hope you had a good time working on this tutorial. And, I hope it will be useful. If you have any questions, suggestions or you find a bug, post a comment or contact me on twitter. I would love to hear from you. Otherwise, thank you very much for your time and see you here again on Friday. Until then, have a great day!

Thank you very much for your time. And, until next time, have a great day!

Did you like this article? Please subscribe.

Are you on social media? Let's connect! You can find me on Twitter and Dribbble.

Leave a Reply