Lazy Image Component Using the Intersection Observer API in Vue.js

Alex Jover Morales

Images, as most of media formats, can be really heavy and take up a large chunk of time to load. We’ve been taught as web developers to compress them as much as possible, having a 2x version for retina displays… and lazy loading them when it makes sense.

The question is, when does it make sense to lazy load an image? Well, if an image is at the top of the page, it’s likely to be visible from the start. In that case, lazy loading isn’t needed. But if an image is visible only later after scrolling, or if it’s part of a list/grid of images (think Google Images) then it makes sense to lazy load them.

Intersection Observer API

In the past, detecting the visibility of an element on a page was hard. Developers needed to implement it themselves or use libraries for that, often with sluggish results and error-prone solutions.

The Intersection Observer API solves this problem in a really neat and performant way. It provides a subscribable model that we can observe to be notified when an element enters the viewport.

Here’s a simple example:

const observer = new IntersectionObserver(entries => {
  const rainbowDiv = entries[0];
  if (rainbowDiv.isIntersecting) {
    // Do something cool here
  }
});

const rainbowDiv = document.querySelector("#rainbowDiv");
observer.observe(rainbowDiv);

We must create an instance of IntersectionObserver, passing the subscribe callback as its parameter. In there we check if it intersects with the rainbowDiv using the method isIntersecting. Finally we must call the observe method, where we pass an element or list of elements.

As you might have noticed, the subscribe callback receives an array of entries, meaning that you can observe multiple elements:

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // ...
    }
  });
});

const allRainbows = document.querySelector(".rainbow");
observer.observe(allRainbows);

Check out this post of ours for another example of using the Intersection Observer API in vanilla JavaScript.

LazyImage Vue Component

Given the example above, you probably have an idea of how a LazyImage.vue component could be implemented:

LazyImage.vue

<template>
  <img :src="srcImage" />
</template>

<script>
export default {
  props: ['src'],
  data: () => ({ observer: null, intersected: false }),
  computed: {
    srcImage() {
      return this.intersected ? this.src : '';
    }
  },
  mounted() {
    this.observer = new IntersectionObserver(entries => {
      const image = entries[0];
      if (image.isIntersecting) {
        this.intersected = true;
      }
    });

    this.observer.observe(this.$el);
  },
}
</script>

We’re setting up the observer in the mounted hook, making sure the component has been attached already to the DOM. We can access the component element by using this.$el and then pass it to the observe method.

Then, we have an intersected flag in the state that we use in a computed property, srcImage, so that when it intersects with the viewport, it will return the actual value of the src prop, but if not it returns an empty string, and the browser loads nothing.

A Note on Performance

Keep in mind that observing elements takes up memory and CPU, that’s why it’s important to stop observing them as soon as we don’t need to.

We have a couple of methods on the IntersectionObserver instance:

  • unobserve: Stops observing an element.
  • disconnect: Stops observing all elements.

Since in our case we only have one element, either of them will work fine:

LazyImage.vue

<template>
  <img :src="srcImage" />
</template>

<script>
export default {
  props: ['src'],
  data: () => ({ observer: null, intersected: false }),
  computed: {
    srcImage() {
      return this.intersected ? this.src : '';
    }
  },
  mounted() {
    this.observer = new IntersectionObserver(entries => {
      const image = entries[0];
      if (image.isIntersecting) {
        this.intersected = true;
        this.observer.disconnect();
      }
    });

    this.observer.observe(this.$el);
  },
  destroyed() {
    this.observer.disconnect();
  }
}
</script>

First, we stop observing right after we set this.intersected = true; because the image is already loaded at that point and it wouldn’t make sense to continue observing.

Additionally, it doesn’t make sense to observe a component if it gets destroyed, that’s why we added the destroyed hook to stop observing it when that happens.

If you try out the LazyImage component with a list of images and open the Network tab in your browser’s DevTools, you’ll see that the images are being loaded as they enter the viewport.

If your interested in lazy loading images in your Vue apps, but not in implementing the intersection observer code yourself, you can check out the vue-clazy-load component.

Wrapping Up

Lazy Loading images can improve your page performance, in particular the page load time and time to interactive performance metrics. With the IntersectionObserver API, it’s both easy and performant to create your own LazyImage component and use it in your apps.

The IntersectionObserver API is not fully supported by all modern browsers just yet, but there’s a polyfill for it maintained by the w3c so you can use it in your apps today.

Stay cool 🦄

  Tweet It

🕵 Search Results

🔎 Searching...