So since April 2020 we have a browser native support to do lazy loading of images which is great, so i decided to write a quick Vue component that uses that functionality.

One problem with the lazy native loading is that it won't display any loading indicator when the image loads.

At the moment only Firefox and Chromium(also Android) based browsers are supporting this feature but it behaves very differently, in Chromium-based browsers it loads far from the end of the viewpoint (a very eager lazy loading) and in Firefox (the image will load only when it enters into the viewport by a lot like 5% or 10% of the IMG element).

Here is a short screen recording with an example where you can see that Chromium-based browser only defer loading for the last 3 pictures in the column while Firefox defers loading till the image directly hits well the viewpoint.

https://youtu.be/h5dI2eTsyYo

Now here's the Vue Component:

<template>
    &lt;div>&lt;!-- Must have only one root element, waiting for VUE3  -->
 &lt;div v-show="!isLoad" class="spinner">&lt;/div>  
&lt;img  loading="lazy" class="card-img-top"  v-bind:src="imgSrc"  :srcset="srcset" v-bind:alt="altContent" v-on:load="loaded">
    &lt;/div>
&lt;/template>
 
&lt;script>
export default {
  data() {
    return {
        altContent:'',
        isLoad: false,
        imgSrc:this.propData.imageSource,
        srcSet:'',
    };
  },
  props: ["propData"],
  created() {
   this.createImgSet();
    this.altContent = `Certificate ${this.propData.certName} image`;
  },
  methods: {
      loaded() {
        this.isLoad = true;
      },
      createImgSet(){
          let lastIndex = this.imgSrc.lastIndexOf('/');
          let picName = this.imgSrc.substr(lastIndex+1);
          let filePath = this.imgSrc.substr(0, lastIndex+1);
          this.srcset = [ filePath+'25/'+picName+ ' 256w', filePath+'50/'+picName+ ' 528w',  filePath+'75/'+picName+ ' 792w', this.imgSrc + ' 1056w',   ].join();
      }
      
      
  }
};
&lt;/script>

&lt;!-- CertImg.vue -->

I use the component like this (after it's registered):

<template>
    &lt;div>&lt;!-- Must have only one root element, waiting for VUE3  -->
 &lt;div v-show="!isLoad" class="spinner">&lt;/div>  
&lt;img  loading="lazy" class="card-img-top"  v-bind:src="imgSrc"  :srcset="srcset" v-bind:alt="altContent" v-on:load="loaded">
    &lt;/div>
&lt;/template>
 
&lt;script>
export default {
  data() {
    return {
        altContent:'',
        isLoad: false,
        imgSrc:this.propData.imageSource,
        srcSet:'',
    };
  },
  props: ["propData"],
  created() {
   this.createImgSet();
    this.altContent = `Certificate ${this.propData.certName} image`;
  },
  methods: {
      loaded() {
        this.isLoad = true;
      },
      createImgSet(){
          let lastIndex = this.imgSrc.lastIndexOf('/');
          let picName = this.imgSrc.substr(lastIndex+1);
          let filePath = this.imgSrc.substr(0, lastIndex+1);
          this.srcset = [ filePath+'25/'+picName+ ' 256w', filePath+'50/'+picName+ ' 528w',  filePath+'75/'+picName+ ' 792w', this.imgSrc + ' 1056w',   ].join();
      }
      
      
  }
};
&lt;/script>

&lt;!-- CertImg.vue -->

Notice that I used the same spinner in the component declaration as for loading the image, which will make sure that the same loading spinner will continue to show for both the process of loading the component and the image inside de component.

One important thing is that if you have a native lazy loaded image that is initially hidden (display: none) the JavaScript onLoad event will not fire.

So even if native lazy loading is not so great at the moment if you don't want to use a Js library you can combine a loader with native lazy loading, and in modern browsers, you end up with much less Js code