How to Build a Simple and Powerful Lazyload JavaScript Plugin

Have you ever wanted to use a lazyload plugin to speed up your website? Who wouldn’t? The problem is most lazyloading plugins require jQuery. Sure, there are some exceptions, 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 can save you a lot of time. However, if you need just one feature, using a big library is not necessary and can be a waste of resources. Think about it. Even the slim version of jQuery is 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!

Table of Contents:

  1. One thing to think about
  2. HTML
  3. CSS (Sass)
  4. JavaScript
    Testing the viewport
    Creating custom fade in effect
    Building the core
    Putting the pieces together

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 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 the lazyload plugin (outside noscript). Then, when we initiate the 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

This is a tutorial about building a 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 the plugin we build is, 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 plugin. We need to explicitly say what attributes are necessary. We will use thedata 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 div elements 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

Here there’s not 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 the 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 an 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 the 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 learned some 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!

Master the skills of design, development and business. Learn the skills you need to become tech and design entrepreneur! Subscribe to my blog.


Originally published on Alex Devero blog.

A List Apart (ISSN: 1534–0295) explores the design, development, and meaning of web content, with a special focus on web standards and best practices.