Lazy Loading Images with the Intersection Observer API


In this tutorial, I'm going to be covering how to use the Intersection Observer API to load images only when needed.

What is lazy loading

Lazy loading is a technique where the loading of a resource is deferred until needed as opposed to during page load. Consider a scenario where an image is only loaded when the user scrolls to the section of the site that has it. It would never be loaded if the user never scrolls there.

Lazy loading can help improve the performance of your websites, since downloading fewer resources upfront would reduce initial load time and make the page usable much earlier. It can also significantly improve the overall user experience.

Using Intersection Observer to lazy load images

The Intersection Observer API provides a way to asynchronously detect when an element has entered the viewport and then perform an action afterward. It can be used for a variety of scenarios. e.g. lazy loading images, implementing infinite scroll, deciding if to perform tasks or animation processes based on if the user will see the result, etc.


This is the markup pattern for the image tag we'd like to lazy load:

<img data-src="image_url.jpg" src="placeholder_image.jpg" alt="image-to-lazy-load" class="lazy"/>

The important attributes of this pattern are:

  • The class attribute: This is used to specify the class name of the image(s) to act on.
  • The data-src attribute: This will contain the desired image URL. We must keep the desired image URL here as opposed to in the src attribute because the browser would immediately trigger loading of any URL placed in the src attribute.
  • The src attribute: This will contain a placeholder image URL. A placeholder appears in place of the desired image until it's fully loaded. It should be a low-quality image, typically a blurred version of the actual image, or a solid color that's dominant in the actual image.

The Intersection Observer can be used as follows:

// Create an instance of observer
const observer = new IntersectionObserver(callback, options);

// start observing

The simple syntax involves creating an Intersection Observer by calling its constructor and passing it a callback that is run whenever the target crosses a particular threshold, which we can specify in options.

The default options are shown below. If you don't pass any options when creating the Intersection Observer, it will be created with these default options.

 const options = {
      root: null, // The element that is used as the viewport for checking visibility of the target. Defaults to the browser viewport when null.
      rootMargin: 0 // The margin value to be used along the sides of the root element to compute intersection.
      threshold: 0  // Indicates at what % of target's visibility the observer's callback should be executed.


  // Create an observer instance and pass the callback and possible options 
  const observer = new IntersectionObserver(callback);  

  // Since we have multiple images we'd like to observe, we can create an array containing each image
  var lazyImages = []".lazy-image"));

  // Loop through the image array and pass each image as a target element to the observer 
  lazyImages.forEach((lazyImage)=> {

The callback receives a list of IntersectionObserverEntry objects and the observer when a threshold is crossed. We can define it as follows:

 function callback(entries, observer){
    entries.forEach((entry) => {
        const target =;
        const dataSrc = target.getAttribute('data-src');        
        if (entry.isIntersecting) {
            // put the desired url(dataSrc) as the src          
            target.setAttribute('src', dataSrc);
           // stop observing the element