Technology Blog

Look deep into latest news and innovations happening in the Tech industry with our highly informational blog.

Create a lazy-loading image component for faster Vue.js apps

hkis

 
Before we get started, let’s first understand what we are building. The goal is to create a Vue.js component to display an image, but it should only download that image when it is in the viewport. In other words, if we have a large list of images, only the ones the user actually needs to see will be downloaded. This will speed up our apps.

Here’s the goals:

  • Should function as a drop-in replacement for <img> tags
  • Lazy-load images when they scroll into view
  • Shows a placeholder while the image is downloading
  • No external dependencies

Basic initial template

We know we will need an <img> tag, so let’s start there. For accessibility purposes, images should always have an alt attribute, even if it’s empty. We can easily pull the alt attribute from the $attrs or fall back to an empty string.

<template>
  <img :alt="$attrs.alt || ''" />
</template>

Tiny transparent placeholder

Now, something we need to fix is that we don’t actually want to set the image src attribute right away because that would cause it to download immediately.

Instead, what we want is a tiny transparent PNG that we can use as a placeholder for the image src. Using a placeholder instead of an empty value will cause the image to take up space on the page while it waits to download the real image.

For this to work properly, we need to hijack the src attribute by expecting it as a prop. We also need to tap into the <img> width and height attributes to create a tiny transparent PNG with an HTML canvas, and export it as a computed property called dataUrl.

<template>
  <img
    :src="dataUrl"
    :alt="$attrs.alt || ''"
  />
</template>
<script>
export default {
  props: {
    src: {
      type: String,
      required: true,
    },
  },

  computed: {
    dataUrl() {
      const { width, height } = this.$attrs
      if (!width || !height) return ""

      // create a tiny png with matching aspect ratio as img
      const w = 100
      const canvas = document.createElement("canvas")
      canvas.width = w
      canvas.height = (height / width) * w

      return canvas.toDataURL()
    },
  },
}
</script>

Alright, so we’ve got a transparent image when the component lands on the page. Now we need to figure out how to set our src attribute to the real image when the component is in view.

For that, we will hook into the mounted lifecyle event and use Intersection Observer. Before we get to that, however, we need to do a little refactor.

Keeping expected image behavior

We need a <div> to wrap our <img> (This will make more sense later). Divs and images behave slightly different, so we also nee a little bit of style to make sure it still behaves like a native <img>.

<template>
  <div class=" app-img ">
    <img
      :src="dataUrl"
      :alt="$attrs.alt || ''"
      v-bind="$attrs"
    />
  </div>
</template>

<script>
export default {
  inheritAttrs: false,

  props: {
    src: {
      type: String,
      required: true,
    },
  },

  computed: {
    dataUrl() {
      const { width, height } = this.$attrs
      if (!width || !height) return ""

      // create a tiny png with matching aspect ratio as img
      const w = 100
      const canvas = document.createElement("canvas")
      canvas.width = w
      canvas.height = (height / width) * w

      return canvas.toDataURL()
    },
  },
}
</script>

<style>
.app-img {
  display: inline-block;
}
</style>

Any attributes it receives should be bound to our tag and not the <div>. We’ll use v-bind and $attrs for the image, and tell the component not to add the default attributes to the root <div> by setting inheritAttrs to false.

Lazy loading with Intersection Observer

we’re ready to implement the Intersection Observer for lazy-loading. we won’t go into too much detail on how Intersection Observer works, but in the mounted hook, we create an IntersectionObserver that watches for the component (via $el) to enter the viewport. When it does, we add an event handler to the image’s ‘load’ event, and assigns the image’s src attribute (which begins loading it). We also do a bit of cleanup work to the beforeDestroy hook just in case.

<template>
  <div class=" app-img ">
    <img
      :src="dataUrl"
      :alt="$attrs.alt || ''"
      v-bind="$attrs"
    />
  </div>
</template>

<script>
export default {
  inheritAttrs: false,

  props: {
    src: {
      type: String,
      required: true,
    },
  },

  computed: {
    dataUrl() {
      const { width, height } = this.$attrs
      if (!width || !height) return ""

      // create a tiny png with matching aspect ratio as img
      const w = 100
      const canvas = document.createElement("canvas")
      canvas.width = w
      canvas.height = (height / width) * w

      return canvas.toDataURL()
    },
  },

  mounted() {
    const { src, $el } = this

    const observer = new IntersectionObserver(([entry]) => {
      const img = $el.querySelector("img")

      if (entry.isIntersecting) {
        // Element is in viewport
        img.src = src
        observer.disconnect()
      }
    })
    observer.observe($el)

    this.$once("hook:beforeDestroy", () => {
      observer.disconnect()
    })
  },
}
</script>

<style>
.app-img {
  display: inline-block;
}
</style>

Alright, our image starts on the page as a transparent PNG, and when it enters the viewport, it loads the real image. The next thing we need to make it better some placeholder while it waits to load.

Adding visual placeholder content

We’ll add two props for the placeholder so it can either be a background color or a different (tiny) image. We’ll also add the placeholder as a <div> which will be absolutely positioned over the entire component.

We need to know the size of the original image so that we can stretch our placeholder to the same size. If we don’t know the width and height (we’ll cheat and use the dataUrl computed prop), we just won’t show the placeholder:

<template>
  <div class=" app-img ">
    <div
      v-if="dataUrl"
      :style="{ background }"
      class=" app-img__placeholder "
    >
      <img :src="placeholder || dataUrl" alt="" v-bind="$attrs" />
    </div>
    <img
      :src="dataUrl"
      :alt="$attrs.alt || ''"
      v-bind="$attrs"
      class=" app-img__img "
    />
  </div>
</template>

<script>
export default {
  inheritAttrs: false,

  props: {
    src: {
      type: String,
      required: true,
    },
    placeholder: String,
    background: String,
  },

  computed: {
    dataUrl() {
      const { width, height } = this.$attrs
      if (!width || !height) return ""

      // create a tiny png with matching aspect ratio as img
      const w = 100
      const canvas = document.createElement("canvas")
      canvas.width = w
      canvas.height = (height / width) * w

      return canvas.toDataURL()
    },
  },

  mounted() {
    const { src, $el } = this

    const observer = new IntersectionObserver(([entry]) => {
      const img = $el.querySelector(`.app-img__img`)
      const placeholder = $el.querySelector(`.app-img__placeholder`)

      img.onload = function() {
        delete img.onload
        if (placeholder) {
          placeholder.remove()
        }
      }
      if (entry.isIntersecting) {
        // Element is in viewport
        img.src = src
        observer.disconnect()
      }
    })
    observer.observe($el)

    this.$once("hook:beforeDestroy", () => {
      observer.disconnect()
    })
  },
}
</script>

<style>
.app-img {
  display: inline-block;
  position: relative;
}

.app-img__placeholder {
  position: absolute;
}
</style>

A couple of other things to note are the in the intersectionObserver. Once the main image has loaded, we want to remove the placeholder.

Adding final touches

Theres a few more things we can do to make it a better experience.

  • Avoid pixelation on the placeholder by applying a CSS blur filter.
  • Add a fade transition when the main image loads.

We’ll accomplish most of this with CSS, but we do need one class added to the image once it has finished loading. And since we have a CSS transition, we also set a timeout to remove the placeholder. Any time we set a timeout, we also want to make sure we clear it when the component is destroyed.

For more Information and to build the website using Vue.js, Hire Vue.js Developer from us as we give you a high-quality product by utilizing all the latest tools and advanced technology. E-mail us any clock at – hello@hkinfosoft.com or Skype us: “hkinfosoft“.

To develop the custom website using Vue.js, please visit our technology page.

Content Source:

  1. stegosource.com