How to Lazy Load Images With Intersection Observer

How to Lazy Load Images With Intersection Observer

And save network requests and bandwidth
Ferenc Almasi • 🔄 2021 November 11 • 📖 6 min read

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.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScript

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>
lazy.html
Copied to clipboard!

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', () => {
  ...
}
lazy.js
Copied to clipboard!

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));
}
lazy.js
Copied to clipboard!

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.

getting back a node list for queryselector

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.

logging out the intersection observer entry when the 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:

Enable throttling in the network request tab

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.

Lazy loading animation

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();
    });
}
lazy.js
Copied to clipboard!

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
    }
}
lazy.js
Copied to clipboard!

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.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScript

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;
    }
}
lazy.scss
Copied to clipboard!

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.

request tab showing lazy loading images

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:

How to Improve Page Speed by Optimizing Content

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!

Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Unlimited access to hundred of tutorials
  • check Access to exclusive interactive lessons
  • check Remove ads to learn without distractions
Become a Pro

Recommended