Lazy Loading Images in VueJs

Lazy Loading Images in VueJS is important to save bandwidth, rank your page better, improve performance considerably and provide a better User Experience, especially if your website has several images. In this tutorial of Lazy loading images with Vue.js, we will be using an extremely swift and lightweight package called Lozad.js that does the major heavy-lifting behind the scenes.

But Why Lozad, you ask?

  • Lightweight – < 1kb(978 bytes) minified & gzipped.
  • Very performant. Supports images, audios, videos, etc.
  • No dependencies. Uses the latest IntersectionObserver API.
  • For browsers that don’t support this API currently (mainly Safari, but support coming soon), it silently falls back to loading images as usual. (i.e. No lazy loading.) Alternatively, there is this polyfill available. If you’d like, you could use the CDN version instead:
    https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver. This checks the User-Agent and loads the code only when the IntersectionObserver API is unsupported.
  • Lozad also supports lazy-loading of dynamically added elements.

Competition:

lady loading images vuejs

Vue-Lazyload –

Vue-Lazyload is a plugin that can suffice your needs very well. It has several advanced options, however it is not as light as Lozad. (Comes in at ~20kb). Could be good alternative for those who’re okay with this.

Vue Lazy Image –

Lazy Image component is another option that is new and quite good. Since its new, it has limited support and currently does not work well for dynamically added images, like carousels.

The solutions mentioned above, though very good, did not cater to my specific needs. Hence, I decided to go with a basic lightweight solution that just works! (Lozad.js!)


You can get lazy-loading up and running in 3 simple steps:

1. Install, Import Lozad

You can install Lozad via any package manager, or even use the CDN.

$ npm install --save lozad   // with NPM Package Manager 
$ yarn add lozad             // with Yarn Package Manager
$ bower install lozad        // with Bower

// Alternatively, use a CDN
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/lozad/dist/lozad.min.js"></script>

Note that if you use the CDN, you will find the library on window.lozad.

The next thing to do is to import this library into your Vue Component:

// demo.vue
<template>
  // ...
</template>
<script>
    import lozad from 'lozad'
</script>
<style scoped>
 // ...
</style>

2. Create Image Markup in your Vue Component

We will design this component to have some basic markup and styling that includes an image which should be lazy loaded.

// demo.vue
<template>
  <div class="container">
      <div>Hello World</div>
      <img class="lazy-load" data-src="/image.jpg" />
  </div>
</template>
<script>
    import lozad from 'lozad'
    data() {
        return {}
    },
    mounted() {}
  },
</script>
<style scoped>
    .container div {
        height: 100vh;
        width: 100vh;
    }
    .container img {
        margin-top: 300px;
        height: 100px;
        width: 100px;
    }
</style>

As you can see, the image element in the markup has no src attribute. It instead has a data-src attribute (or the data-background-image if you wish to use a background image instead) which contains the path of the image. We also add some minor styling to ensure that the image is placed below the current viewport to trigger lazy loading.

3. Instantiate, Configure Lozad in your Vue Component

We initialize and configure Lozad in the mounted() hook of the component. Why you ask? Its because that’s where the DOM is fully built. And as an example, we will hard code the local state of the component (data()) with the image path which usually should be dynamic depending on your application.

// demo.vue
<template>
  <div class="container">
      <div>Hello World</div>
      <img class="lazy-load" :data-src="imagePath" />
  </div>
</template>
<script>
    import lozad from 'lozad'
    data() {
        return {
            imagePath: "path/to/image.jpg"   // hard-coded for example
        }
    },
    mounted() {
        let elmts = document.querySelectorAll(".lazy-load");
        window.img_observer = lozad(elmts);
        window.img_observer.observe();
    }
  },
</script>
<style>
   // ...
</style>

As seen above, we select the images by class and pass those elements to lozad. Lozad then initializes. We store its return value in the window object. Lozad returns an Object that contains an instance of the IntersectionObserver API (accessible via the observer property if it is supported, else this property is set to null. If you’d like, you could do a console.log('lozad instance', window.img_observer) to see the return value)

Now if you execute your code, lazy-loading should be fully functional. However, for single page applications, this could lead to a memory leak. This is because each time you navigate, it causes the mounted() hook to be run and then the previous watchers that were setup by the IntersectionObserver API are not cleared properly.

To avoid this, we will update the mounted() hook where we will manually clear the previous watchers –

// demo.vue
 mounted() {
        let elmts = document.querySelectorAll(".lazy-load");
        if (window.img_observer && window.img_observer.observer) {
            window.img_observer.observer.disconnect();
            delete window.img_observer;
            for (let i = 0, len = elmts.length; i < len; i++) {
                // load image only when the image src is updated
                const img = elmts[i].getAttribute('data-src') || false;
                if (img && 
                    elmts[i].getAttribute('data-loaded') === 'true' && 
                    img !== elmts[i].getAttribute('src')) 
                   {
                       elmts[i].setAttribute('data-loaded', false);
                   }
            }
        }
        window.img_observer = lozad(elmts);
        window.img_observer.observe();
    }

So, this is what happens each time we navigate to a different route:

– We check if lozad was previously initialized. We also check whether the observer was setup or not. If yes, we call on observer.disconnect() that causes to stop watching all target elements. This ensures that there will be no memory leaks.
– We then delete the previous Lozad instance.
– Thereafter, we loop across all the elements to verify that the images being displayed are not the same as the previous ones and update the attributes accordingly.
– We initialize lozad again and start observing these new elements.


That’s it! Now you have a fully functional App equipped with Lazy Loading! Here’s a website where lazy loading is incorporated – GospelMusic.io. Feel free to checkout the network tab to verify the same.

For the sake of integrity, here’s a snippet of the entire component:

// demo.vue
<template>
  <div class="container">
      <div>Hello World</div>
      <img class="lazy-load" :data-src="imagePath" />
  </div>
</template>
<script>
    import lozad from 'lozad'
    data() {
        return {
            imagePath: "path/to/image.jpg"   // hardcoded only as an example
        }
    },
    mounted() {
        let elmts = document.querySelectorAll(".lazy-load");
        if (window.img_observer && window.img_observer.observer) {
            window.img_observer.observer.disconnect();
            delete window.img_observer;
            for (let i = 0, len = elmts.length; i < len; i++) {
                // load image only when the image src is updated
                const img = elmts[i].getAttribute('data-src') || false;
                if (img && 
                    elmts[i].getAttribute('data-loaded') === 'true' && 
                    img !== elmts[i].getAttribute('src')) 
                   {
                       elmts[i].setAttribute('data-loaded', false);
                   }
            }
        }
        window.img_observer = lozad(elmts);
        window.img_observer.observe();
    }
  },
</script>
<style scoped>
    .container div {
        height: 100vh;
        width: 100vh;
    }
    .container img {
        margin-top: 300px;
        height: 100px;
        width: 100px;
    }
</style>

And here’s a live JS fiddle that you can checkout:

P.S. When your app grows, you might have to duplicate the code in the mounted hook to ensure proper working of lazy loading. Now that’s redundant and terrible. To avoid that, you could extract the code from it into a service and then just call the service from the mounted hook 😉
Hope that helps!

Going Further –

If you decide to turn this into a component, here’s an excellent tutorial that also uses Lozad.js.


References