
How to Lazy Load Images With Intersection Observer
In todayās world, a significant amount of transferred bytes can be accounted for images. This means that we can gain the most on performance by optimizing them. Yet, most of the time these resources are kept hidden and not even seen by visitors. Offscreen images are requested, but before they are scrolled into view, users bounce off.
This is where lazy loading can help us. It defers the loading of offscreen images that are only requested once they are in the viewport. This way, we can save precious bytes, reduce page load times, and potentially save money for the visitor if they have a limited data plan. For this, we will use theĀ Intersection Observer API, which provides a way for us to detect if our target element is intersecting with either other elements or the viewport.

Using IntersectionObserver
When lazy loading images, probably the simplest approach is to:
- Reference a placeholder image in theĀ
img
āsĀsrc
Ā attribute - Add the URL for the image we want to lazy load into aĀ
data-src
Ā attribute - Replace theĀ
src
Ā value with the value insideĀdata-src
Ā once the image is in the viewport
But we could even further enhance this: Instead of using a placeholder image, we can use a CSS loading animation. This way, we wonāt have to load in the placeholder image. This requires one additional step though: remove the loading DOM element once the image is ready. Letās see how we can do both. Take the following structure as an example:
<div class="lazy-image">
<div class="placeholder-image">
<i class="icon-loading"></i>
</div>
<img src="placeholder.png" data-src="final.gif" />
</div>
First, you will have to wait for the DOM to be fully loaded. For this we will need to wrap everything into an event listener:
document.addEventListener('DOMContentLoaded', () => {
...
}
If you are using React like myself, the above part can be skipped and you can inject the code straight into theĀ componentDidMount
Ā method. Which will be the following:
const images = Array.from(document.querySelectorAll('.lazy-image img'));
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const image = entry.target;
image.src = image.dataset.src;
image.onload = () => image.previousElementSibling.remove();
imageObserver.unobserve(image);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
Letās break down what we are doing. First, we need to query all the images that need to be lazy-loaded. We also have to transform it into an array since weāre going to get back aĀ NodeList
Ā and we canāt loop on that.

Next, we need to make sure thatĀ IntersectionObserver
Ā is supported. If it is, we can create aĀ new IntersectionObserver
Ā which takes in a callback function as a parameter. This will run every time the observed element gets near the viewport.

We attach the observers on line:17 withĀ imageObserver.observe
. Then we simply have to loop through the entries and check if one of themĀ isIntersecting
. Here we can change the source and remove the loading indicator once the image has been loaded. Lastly, we can remove the observer by callingĀ unobserve
Ā on theĀ Observer
.
To verify if everything works, go to the Network tab and enable throttling by changing āOnlineā to something else from the presets or creating your own one:

Now you have some time to easily see how lazy-loaded images are being requested. And we can also verify whether the loading script works.

Using Fallback
This wonāt work if the browser doesnāt supportĀ IntersectionObserver
. Although global support is around 92%, IE doesnāt support it at all. If you have traffic in the millions, 1ā2% can still mean tens of thousands of users.
The simplest approach here is to load the images right away as we normally would:
if ('IntersectionObserver' in window) {
...
} else {
images.forEach(img => {
img.src = img.dataset.src;
img.previousElementSibling.remove();
});
}
This of course would mean that unsupported browsers wonāt get the same benefits as the rest. Alternatively, we can create our own function for checking if an element is in the viewport:
const inViewport = selector => {
const element = document.querySelector(selector)
const elementBoundingClientRect = element && element.getBoundingClientRect();
const offset = 50;
if (elementBoundingClientRect) {
return (elementBoundingClientRect.top - offset >= 0 && elementBoundingClientRect.bottom > 0 && elementBoundingClientRect.bottom + offset <= window.innerHeight);
} else {
return false;
}
}
document.onscroll = () => {
if (inViewport('.lazy-image')) {
// replace image sources
}
}
This can be done usingĀ getBoundingClientRect
. We need to attach an event listener to the scroll event. To improve performance you can add a timeout to only fire the event every once in a while. With this solution, we managed to cover older browsers.

The CSS Approach
We can also use a similar approach to get the same behavior for images loaded through CSS. We have an even easier way of doing things here. All we have to do is style the nodes without background images first:
.lazy-content {
background: cornflowerblue;
&.loaded {
background: url(lazy.jpg) cornflowerblue;
}
}
Then from JavaScript, we simply have to add the additional class to start requesting the resource.
Testing it out
All thatās left to do is to confirm it through the Network tab. Donāt forget to enable throttling and change the filtering toĀ Img
Ā only. After some scrolling, we can see images requested one after the other.

This will ensure that you cut down on requests, the number of bytes transferred and you will get a faster initial page load which will improve user experience for your visitors. When images get requested though, you can still make adjustments to it to save as much as you can. If you would like to learn more about image optimization techniques, you can check out my article onĀ content optimization:

Have you tried lazy loading before? Are you dealing with images in another way? Let us know in the comments. Thank you for reading through, happy optimization!
Access exclusive interactive lessons
Unlimited access to hundreds of tutorials
Remove ads to learn without distractions