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
‘ssrc
attribute - Add the URL for the image we want to lazy load into a
data-src
attribute - Replace the
src
value with the value insidedata-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!
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: