DEV Community

Saul Hardman
Saul Hardman

Posted on • Originally published at viewsource.io on

Lazy Loading Images in Nuxt

The lazy loading of images via the loading attribute has landed in Chrome, and other browser-vendors are sure to follow suit. Deferring to the browser when support is available and otherwise loading a polyfill such as Lazysizes is a solid approach to performant, responsive images.

Checking the HTMLImageElement for the loading property is a reliable way to test for native lazy loading support:-

const supportsLoadingAttribute = "loading" in HTMLImageElement.prototype;
Enter fullscreen mode Exit fullscreen mode

If the browser supports native image loading we do nothing, or else we dynamically import() the Lazysizes module. Authoring this code within a client-side only Nuxt plugin means the polyfill loads and initialises only once and within the context of the entire application:-

// ~/plugins/lazysizes.client.js

export default () => {
  if ("loading" in HTMLImageElement.prototype) {
    return;
  }

  import("lazysizes");
};
Enter fullscreen mode Exit fullscreen mode

Below is a loosely outlined ResponsiveImage component which follows the pattern that I want to demonstrate.

The server-side rendered HTML contains an image with the src and srcset values assigned to data-* attributes – the actual attributes contain placeholders. On mount() (a client-side only Vue lifecycle hook) if the browser supports the loading attribute the placeholders are replaced by the true src and srcset values. If support is absent then the class 'lazyload' is added to the <img> and Lazysizes takes over from there:-

<!-- ~/components/ResponsiveImage.vue -->
<template>
  <img
    :class="{ lazyload: loading === 'lazy' && !supportsLoadingAttribute }"
    :loading="loading"
    v-bind="{ ...sources }"
  />
</template>

<script>
  // base64-encoded transparent GIF
  const placeholder =
    "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";

  export default {
    props: {
      // the props required to compute `srcset` should go here

      loading: {
        type: String,

        default: "lazy"
      }
    },

    data() {
      return {
        supportsLoadingAttribute: false
      };
    },

    computed: {
      src() {
        // `return` a fallback image for browsers
        // that don't support `srcset` and `sizes`
      },

      srcset() {
        // responsive images can be handled in all sorts of
        // ways and I won't go into any further detail here
      },

      sources() {
        if (this.loading === "lazy" && !this.supportsLoadingAttribute) {
          return {
            "data-src": this.src,
            "data-srcset": this.srcset,

            src: placeholder,
            srcset: `${placeholder} 1w`
          };
        }

        return {
          srcset: this.srcset
        };
      }
    },

    mounted() {
      this.supportsLoadingAttribute = "loading" in HTMLImageElement.prototype;
    }
  };
</script>
Enter fullscreen mode Exit fullscreen mode

There's are many different approaches to lazy-loading images on the web. Each has its advantages and disadvantages and the one you choose will ultimately depend on your priorities. Are you more concerned about SEO, page speed, data footprint, or browser compatibility?

The pattern outlined above, for example, would need to provide a <noscript> fallback in the case of JavaScript being disabled.

Either way, hopefully this has started you off in the right direction. Check out the links below for some more in-depth explanations of the loading attribute and lazy-loading markup patterns.

Further Reading

Top comments (2)

Collapse
 
jeckojim profile image
jeckojim • Edited

Hi Saul,
After looking in a lot of example, I found your article really promising on the best way to implement it. Since then I try to apply it but with multiple "source" elements in a "picture" element. I was thinking doing with something like the folllowing but i'm a bit stuck (I'm not use to do this kind of computed manipulations):

// parent component where the component is called
<TheLazyResponsiveImage
    :imgId="'intro-hero-img'"
    :imgName="'intro-hero-image'"
    :imgSourceTypes="[webp, png]"
    :imgSourceTypeFallback="png"
    :srcsetSizes="{'943w':'944x640', '768w':'769x521', '511w': '512x347'}"
/>
//---------------
// TheLazyResponsiveImage.vue
<template>
//if there is imgSourceTypes prop, I use the<picture> element.
  <picture v-if="imgSourceTypes">
    <source
      v-for="imgSourceType in imgSourceTypes"
      :key="imgSourceType"
      :type="'image/' + imgSourceType"
      :srcset="srcset()"
    >
    <img
      :id="imgId"
      :src="src()"
      :alt="$t('brand_name') + ' ' + name"
      class="product-img is-block"
    >
  </picture>
// I would use your sources method just for regular <img> element
  <img v-else
    :class="{ lazyload: loading === 'lazy' && !supportsLoadingAttribute }"
    :loading="loading"
    v-bind="{ ...sources }"
  >...

//------
// Javascript
<script>
let supportsLoadingAttribute = false

if (process.client) {
  supportsLoadingAttribute = require('~/assets/js/supports/loading-attribute').default
}

export default {
  props: {
    imgId: {
      type: String,
      required: false,
      default: ''
    },
    imgName: {
      type: String,
      required: true,
      default: ''
    },
    imgSourceTypes: {
      type: Array,
      required: true,
      default () {}
    },
    imgSourceTypeFallback: {
      type: String,
      required: true,
      default: 'png'
    },
    srcsetSizes: {
      type: Object,
      required: true,
      default () {}
    },
    placeholder: {
      type: String,
      required: false,
      default: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
    },
    alt: {
      type: String,
      required: false,
      default: ""
    },
    loading: {
      type: String,
      required: false,
      default: 'lazy'
    }
  },

  data () {
    return {
      supportsLoadingAttribute: false
    }
  },

  computed: {
    src () {
      return require(`@assets/img/${this.imgName}-${Object.keys(this.srcsetSizes)[0]}.${this.imgSourceTypeFallback}`)
    },
// this is where I'm not sure on how to proceed, I need to 
    srcset (imgType) {
      let dataSrcset = ''
      let srcsetSizesCounter = 0
      this.srcsetSizes.forEach((srcsetImgDimension, srcsetWidthKey) =­> {
        dataSrcset += `${require(`@assets/img/${this.imgName}-${srcsetImgDimension}.${imgType}`)} ${srcsetWidthKey}`
        srcsetSizesCounter++
      }
      return dataSrcset    
   }
// I would use your sources method just for regular <img> element
   ...
  },

  mounted () {
    this.supportsLoadingAttribute = supportsLoadingAttribute
  }
}

If you can guide me it will be great, if not I think I will try another way.
Maybe there is syntax errors in code but I just wanted to show the concept.
Thank you for your article again.

Collapse
 
saul profile image
Saul Hardman

Hi @jeckojim , for some reason DEV didn't alert me to your comment (perhaps because of how new your account is.)

I'll take a look at your example and get back to you as soon as possible with some suggestions – have a nice weekend 👋