UI Development

Lazy Loading with Intersection Observer

When we open a website today, the most common thing that we see is images. Images are visually appealing to the eyes and grab the attention of a user. It creates an atmosphere for the website and conveys meaning effortlessly.

But, more the number of images on a website heavier the load. It may slow down the load time for your webpage. And, every web developer knows the 3-second rule. You may have high-quality content, but if your website doesn’t load in the first three seconds, there is a 60% chance that the user would abandon the website. A significant aspect of your website is it’s ranking by major search engines depending on how relevant the content of your website is or more popularly known as SEO.

Optimized websites deliver quality content to search engines which in turn provides a better ranking for your website making it available on the searches. Since a considerable part of the website comprises of images, here are a few standard methods to optimize the images for a website:

  1. The type of image must be JPEG or PNG.
  2. An image should have an optimal size, dimension, and resolution.
  3. Never forget to write an alt text for the image you use on a website.
  4. Give meaningful names to your images.

Lazy Loading is a design pattern. It essentially states that there is no need to load everything at once. We need to shelve the assets until we reach the requirement. If used suitably, it can help enhance your website’s performance immeasurably.

Intersection Observer

We need to understand what do we mean when we say shelve the assets. Once a webpage loads, the area visible to the user is called the Viewport. When a user scrolls down, the new area that appears will be the user’s viewport. So, we need to load the assets in the same manner.

Let’s take an example, say a website contains 5 images. On the initial load, there is only one image visible to the user or only a single image is present in the viewport. So, we need to load only that single image. Now as the user scrolls down, three images are to be shown, so we load that three assets only when the user reaches the viewport. This way we accelerate the speed of a website.

Intersection Observer is a new API that helps in determining whether the image is in the user’s viewport.

Let’s see the syntax

const imageObserver = new IntersectionObserver(...)

In this instance or constructor of the Intersection Observer(IO), we will define a function which mainly has two parameters:

const imageObserver = new IntersectionObserver((entriesArr, imgObserver) => {
    // write code
});
  1. entriesArr – is an array of all images that need to be loaded
  2. imgObserver – an instance of IO

Inside the function, we loop through this array of images. As soon as the image intersects with the user’s viewport, we swap the values of data-src attribute with the src attribute. To check if the asset intersects with the current viewport, we use a method called isIntersecting.  This is the definition of IO.

Now we move towards the next step which is to call this definition. IO says that we now need to observe these assets. Also, an important point is to give the same class to all the images that need to be lazy-loaded. imageObserver.observer listens at all times whether an element with the class lazy_img is intersecting with the browser or not and if it is intersecting, then it calls the IO definition.

const imgArr = document.querySelectorAll('img.lzy_img');
imgArr.forEach((img) => {
    imageObserver.observe(img);
});

To see this in action, you can download a few images from https://www.flickr.com/ or https://pixabay.com/ or use any existing images on your laptop. You need a good editor. I prefer VS Code. Also, you will need Node and http-server. I simply downloaded a few pokemon images and wrote some basic HTML for them to show up. I am sharing the code below.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Pokemon</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header class="poke-header">
        <img src="images/pokemon-title.png" alt="pokemon" class="poke-header__img" />
    </header>
    <main class="poke-main">
        <ul class="poke-main__parent">
            <li class="poke-main__parent__child">
                <div class="poke-main__parent__child__img">
                    <img class="lzy_img" src="images/pokemon.jpg" data-src="images/pikachu.png" alt="pikachu" />
                </div>
                <div class="poke-main__parent__child__desc">
                    <div class="poke-desc poke-desc--warning">
                        <span class="poke-desc__warning">Warning</span>
                        <p class="poke-desc__fact">Contact with the Pokémon may cause paralysis.</p>
                    </div>
                    <div class="poke-desc poke-desc--evolution">
                        <p class="poke-desc__evolution">Evolution</p>
                        <div class="poke-desc__img">
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/pichu.png" alt="pichu" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/pikachu.png" alt="pikachu" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/raichu.png" alt="raichu" />
                        </div>
                    </div>
                </div>
            </li>
            <li class="poke-main__parent__child">
                <div class="poke-main__parent__child__img">
                    <img class="lzy_img" src="images/pokemon.jpg" data-src="images/bulbasur.png" alt="bulbasur" />
                </div>
                <div class="poke-main__parent__child__desc">
                    <div class="poke-desc poke-desc--warning">
                        <span class="poke-desc__warning">Warning</span>
                        <p class="poke-desc__fact">Powers up Grass-type moves when the Pokémon is in trouble.</p>
                    </div>
                    <div class="poke-desc poke-desc--evolution">
                        <p class="poke-desc__evolution">Evolution</p>
                        <div class="poke-desc__img">
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/bulbasur.png" alt="bulbasur" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/ivysaur.png" alt="ivysaur" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/venusaur.png" alt="venusaur" />
                        </div>
                    </div>
                </div>
            </li>
            <li class="poke-main__parent__child">
                <div class="poke-main__parent__child__img">
                    <img class="lzy_img" src="images/pokemon.jpg" data-src="images/charmender.png" alt="charmender" />
                </div>
                <div class="poke-main__parent__child__desc">
                    <div class="poke-desc poke-desc--warning">
                        <span class="poke-desc__warning">Warning</span>
                        <p class="poke-desc__fact">Powers up Fire-type moves when the Pokémon is in trouble.</p>
                    </div>
                    <div class="poke-desc poke-desc--evolution">
                        <p class="poke-desc__evolution">Evolution</p>
                        <div class="poke-desc__img">
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/charmender.png" alt="charmender" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/charmeleon.png" alt="charmeleon" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/charizard.png" alt="charizard" />
                        </div>
                    </div>
                </div>
            </li>
            <li class="poke-main__parent__child">
                <div class="poke-main__parent__child__img">
                    <img class="lzy_img" src="images/pokemon.jpg" data-src="images/jigglypuff.png" alt="jigglypuff" />
                </div>
                <div class="poke-main__parent__child__desc">
                    <div class="poke-desc poke-desc--warning">
                        <span class="poke-desc__warning">Warning</span>
                        <p class="poke-desc__fact">Contact with the Pokémon may cause sleeping.</p>
                    </div>
                    <div class="poke-desc poke-desc--evolution">
                        <p class="poke-desc__evolution">Evolution</p>
                        <div class="poke-desc__img">
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/igglybuff.png" alt="igglybuff" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/jigglypuff.png" alt="jigglypuff" />
                            <img class="lzy_img" src="images/pokemon.jpg" data-src="images/wigglytuff.png" alt="wigglytuff" />
                        </div>
                    </div>
                </div>
            </li>
        </ul>
    </main>
    <footer>
        <p>For more information
            <a href="https://www.pokemon.com/us/pokedex/">Click Here</a>
        </p>
    </footer>
    <script src="js/global.js"></script>
</body>
</html>

 

Now that we have seen the markup, let’s see the js file where the actual magic happens.

document.addEventListener("DOMContentLoaded", function () {
    const imageObserver = new IntersectionObserver((entriesArr, imgObserver) => {
        entriesArr.forEach((entry) => {
            if (entry.isIntersecting) {
                const lazyImg = entry.target;
                console.log("lazy loading ", lazyImg);
                lazyImg.src = lazyImg.dataset.src;
            }
        });
    });
    const pokeImgArr = document.querySelectorAll('img.lzy_img')
    pokeImgArr .forEach((pokeImg) => {
        imageObserver.observe(pokeImg);
    });
});

Let’s do a quick npm init and then install http-server. Refer to the screenshots.

If you go back to your editor, you will see that a new file package.json has been added and you need to add the following code for http-server to work:

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

Now run the command npm run start.

Go ahead and open your favorite browser(it’s chrome ) and head to http://127.0.0.1:8080/. If you notice, I have put in a console statement to verify if the image gets loaded only when it is in the viewport.

As we scroll down, new images come into the viewport and hence they get loaded.

Easy right? But if we put a little more thought into it every time the user changes the viewport by scrolling up or down, we are asking the browser to load that same image or a set of images again and again. This is not a good practice, because once all assets are loaded, its a waste of time to load those again and this will sharply decrease the performance of the website.

So, the images must be unobserved as well. To achieve that we add two more lines to the existing code.

document.addEventListener("DOMContentLoaded", function () {
    console.log('content loaded');
    const imageObserver = new IntersectionObserver((entriesArr, imgObserver) => {
        entriesArr.forEach((entry) => {
            if (entry.isIntersecting) {
                const lazyImg = entry.target;
                console.log("lazy loading ", lazyImg);
                lazyImg.src = lazyImg.dataset.src;
                // to make sure assets are not loaded more than once
                lazyImg.classList.remove("lzy_img");
                imgObserver.unobserve(lazyImg);
            }
        })
    });
    const pokeImgArr = document.querySelectorAll('img.lzy_img')
    pokeImgArr .forEach((pokeImg) => {
        imageObserver.observe(pokeImg);
    });
});

 

So, now once the image intersects with the browser, IO loads it and at the same time removes the common class and unobserve that particular target image.

IO can also be used in collaboration with frameworks like React (react-intersection-observer npm), Vue (vue-intersect, infinite scrolling), etc. In conclusion, Lazy Loading is an important aspect of minimal network costs. So, it’s important we as developers do our bit to achieve that.

About The Author