Lazy Loading Images using the Intersection Observer API

Learn how to use the Intersection Observer API to load images only when they appear in the viewport

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces

--

Most webpages today contain images. They make up a huge percentage of what we see on the web. The more the images, the more lag in load time we will experience in our webpage.

We see that a lot of images on our website have a huge impact on our site performance. Our initial page load will suffer greatly. Lazy loading is a concept introduced to curb the loading of resources in page load time.

Before we go on into what is “Lazy Loading”, I’d like to recommend Bit (GitHub), a tool that helps me turn my code into shareable and reusable components — thereby, allowing me to code better and faster.

What is Lazy Loading?

The most important concepts of application performance are Response Time, and Resources Consumption. It is inevitable that they are going to happen. Also, a problem can arise from anywhere though, but it is highly important to find and address them before they happen.

The prospect of Lazy Loading in the web helps reduce the risk of some of the web app performance problems to a minimal. Lazy Loading does well to check the concepts we listed above:

  1. Response Time: This is the amount of time it takes the web application to load and the UI interface to be responsive to users. Lazy loading optimizes response time by code splitting and loading the desired bundle.
  2. Resources Consumption: Humans are impatient creatures if a website takes more than 3 seconds to load, 70% of us will give up. Web apps should not take this long to load. So, to reduce the amount of resources loading, lazy loading loads the code bundle necessary at a time.

Lazy loading speeds up our application load time by loading our web app resources on demand.

Advantages of lazy loading:

  • High performance in bootstrap time on initial load.
  • Smaller resources to download on initial load.

Lazy Loading Images using Intersection Observer

It makes no sense to load images or resources that are not yet visible to the user. First, the images within the viewport are loaded, then, on scrolling by the user any image(s) that comes within the viewport is loaded and placed in the view.

Looking at the picture above. We see a web browser and a web page loaded. We see that #IMG_1 and #IMG_2 is within the viewport, within the viewport means that it is visible to the user and is placed in the borders of the browser's view.

Loading all the images #IMG_1 #IMG_2 #IMG_3 #IMG_4 once the webpage loads isn't ideal, #IMG_1 and #IMG_2 are the only images visible to the user, #IMG_3 and #IMG_4 are not yet visible to the user. So, it would have a positive impact on the site performance if the images #IMG_1 and #IMG_2 are loaded first and #IMG_ 3 #IMG_4 not loaded. When the user scrolls down to bring #IMG_3 visible, it is then loaded. If further scrolls bring #IMG_4 to view then it is loaded.

So, how do we detect when an element

comes into view? Modern browser brings us a new API to help us detect when an element enters the viewport. The API is the Intersection Observer.

Intersection Observer

To observe when

s comes into view. The Intersection Observer API provides a way for us to asynchronously observe changes in the visibility of our elements or the relative visibility of two elements in relation to each other.

To lazy load our images, we have to define

element with some markup pattern:

<img class="lzy_img" src="lazy_img.jpg" data-src="real_img.jpg" />

This is how our lazy loaded image element will be like.

The class identifies the element as a lazily loaded img element. The src attribute gives the lazy load image the initial image before the real image is loaded. The data-src holds the real image that will be loaded to the element when it enters within the browser’s viewport.

Now, let’s write our lazy load logic. As we already stated we will be using the Intersection Observer to detect an element’s visibility in relation to the document in the browser.

First, we create an instance of the Intersection Observer:

const imageObserver = new IntersectionObserver(...);

The IO takes a function in its constructor, this function has two parameters; one holds an array consisting of the element it is to observe, the second param holds the instance of the IO.

const imageObserver = new IntersectionObserver((entries, imgObserver) => {
//...
});
  • entries: holds in an array the elements that are within the browser’s viewport.
  • imgObserver: An instance of the IntersectionObserver

As entries is an array, we have to loop through it to get the elements inside it and perform lazy loading on each of them.

const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach((entry) => {
//...
})
});

Then, for each entry, we will check whether it is within the viewport, if it is intersecting with the viewport we will set the src attribute in the img element to the value of the data-src attribute

const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach((entry) => {
if(entry.isIntersecting) {
const lazyImage = entry.target
lazyImage.src = lazyImage.dataset.src
}
})
});

We check if the entry is within the browser’s viewport if(entry.isIntersecting) {...}, if it is true we store the img element HTMLImgElement instance in lazyImage variable. Next, we set the src attribute by assigning it to the value of the src dataset, with this the image stored in data-src is loaded to the img element. In our browser, the previous image lazy_img.jpg is replaced by the real image.

Now, we need to call our imageObserver to start observing our img elements:

imageObserver.observe(document.querySelectorAll('img.lzy_img'));

We aggregate all the

elments with lzy_img class in our document; document.querySelectorAll('img.lzy_img') and pass it to imageObserver.observer(...).

imageObserver.observer(...) picks the array of

and listens on them to know when their visibility intersects with the browser.

To see our lazy load demo work out, we need to start a Node project:

mkdir lzy_img
cd lzy_img
npm init -y

create an index.html file:

touch index.html

Now add the following contents to it:

<html>
<title>Lazy Load Images</title>
<body>
<div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_1.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_2.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_3.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_4.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_5.jpg" />
<hr />
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target
console.log("lazy loading ", lazyImage)
lazyImage.src = lazyImage.dataset.src
}
})
});
const arr = document.querySelectorAll('img.lzy_img')
arr.forEach((v) => {
imageObserver.observe(v);
})
})
</script>
</body>
</html>

See we have 5 lazy images

s, each with the same lazy image lazy_img.jpg and also different real images to load.

You have to create your own images:

lazy_img.jpgimg_1.jpg
img_2.jpg
img_3.jpg
img_4.jpg
img_5.jpg

Me, I built my lazy_img.jpg in Windows Paint and got the real images (img_*.jpg) from Pixabay.com.

Notice I added a console.log in our IntersectionObserver callback function, this would enable us to know which image is being lazily loaded.

To serve our index.html file we need to install http-server:

npm i http-server

Now we add a start property in the scripts section in package.json

"scripts": {
"start": "http-server ./"
}

Now run npm run start in your terminal.

Open your fav browser and navigate to 127.0.0.1:8080. Our index.html will load and look like this:

You see the imgs are showing the lazy_img.jpg, now since <img class="lzy_img" src="lazy_img.jpg" data-src="img_1.jpg" /> is within the viewport, its real image img_1.jpg will be loaded.

others are not loaded because they are not yet within view of the browser.

We have a problem here, when we scroll an img into a view it loads its image when next we scroll the same img into view the browser attempts to load the real image again.

This will seriously impact on our site performance.

To solve this problem we need to unsubscribe the IntersectionObserver from an img already loaded with the real image and also remove the lzy class from the img element.

So we will edit our code to his:

<script>
document.addEventListener("DOMContentLoaded", function() {
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target
console.log("lazy loading ", lazyImage)
lazyImage.src = lazyImage.dataset.src
lazyImage.classList.remove("lzy")
imgObserver.unobserve(lazyImage)
}
})
});
// ...
})
</script>

Here, once an

is loaded its class name is removed lazyImage.classList.remove("lzy") and its is removed from the IntersectionObserver list imgObserver.unobserve(lazyImage).

Full Code

<html>
<title>Lazy Load Images</title>
<body>
<div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_1.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_2.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_3.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_4.jpg" />
<hr />
</div>
<div style="">
<img class="lzy_img" src="lazy_img.jpg" data-src="img_5.jpg" />
<hr />
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target
console.log("lazy loading ", lazyImage)
lazyImage.src = lazyImage.dataset.src
lazyImage.classList.remove("lzy_img");
imgObserver.unobserve(lazyImage);
}
})
});
const arr = document.querySelectorAll('img.lzy_img')
arr.forEach((v) => {
imageObserver.observe(v);
})
})
</script>
</body>
</html>

Conclusion

We saw behind the scenes how to lazy images using IntersecionObserver API. Next, we implemented a demo to show how to go about setting up your lazy loading code in JS.

Lazy loading can seriously lower performance costs in our web pages, it is undoubtedly a major optimization trick to reduce network costs.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me

Thanks !!!

Learn More

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕