Lazy load images with the IntersectionObserver API

by Dennis — 3 minutes

Lazy loading of images can be a really nice feature for pages with large images. Usually, to create a lazy load functionality you have to use JavaScript. With a swap from a data-src to a src attribute you can already achieve this. A better approach would be using native functionality of the browser. This will increase performance as JavaScript listeners won’t block the user from loading new images. Google Chrome already offers this option. With a loading attribute on your image, the image will be lazy loaded without any JavaScript. The options for this attribute are:

  • eager: Will load the resource immediately, regardless of where it's located on the page.
  • auto: Will fall back to the browsers default value, which is the same as not including the attribute
  • lazy: Will lazy load the image when it - the screen of the user - reaches calculated distance from the image
<img src="image.png" loading="lazy" width="200" height="200" />

The loading attribute requires a width and height to be set. Without dimensions specified, layout shifts can occur, which are more noticeable on pages that take some time to load. Unfortunately the support is very limited.

Can I use example of loading attribute

For this reason I came up with a really simple lazy load JavaScript implementation that uses the IntersectionObserver API as recommended by Google as a fallback. The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport. It has a better support than the loading attribute.

Can I use example of loading attribute

The src attribute is replaced by a data-src to prevent the browser from loading the image immediately

<img lazy-load alt="Vue logo" data-src="./logo.png" />
const images = document.querySelectorAll("[lazy-load]");

images.forEach((el) => {
  const imageObserver = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const lazyImage = entry.target as HTMLImageElement;
        if (lazyImage.dataset.src) {
          lazyImage.src = lazyImage.dataset.src;
          imageObserver.unobserve(lazyImage);
        }
      }
    });
  });
  imageObserver.observe(el);
});

This will collect all images with the lazy-load attribute and it will change the data-src to the original source. You can even add a loading image as a placeholder, be sure to keep the size of image small.

<img lazy-load alt="Vue logo" data-src="/logo.png" src=”/placeholder.jpg” />

In Vue you can create a directive for this. The following example is created for Vue 3 but you can adapt it to Vue 2 as well.

import { createApp } from "vue";
import type { Directive } from "vue";

import App from "./App.vue";

const LazyLoadDirective: Directive = {
  mounted(el) {
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const lazyImage = entry.target as HTMLImageElement;
          if (lazyImage.dataset.src) {
            lazyImage.src = lazyImage.dataset.src;
            imageObserver.unobserve(el);
          }
        }
      });
    });
    imageObserver.observe(el);
  },
};

const app = createApp(App);

// Load directive and mount app
app.directive("lazy", LazyLoadDirective);
app.mount("#app");

In your Vue template you can now use the following syntax:

<img v-lazy alt="Vue logo" data-src="./logo.png" src="/placeholder.jpg" />

Result

With this code I created a small example in Vue 3 with multiple logos with a padding-top of 50vh so you can scroll to the new image and see the effect.

Result of plugin

meerdivotion

Cases

Blogs

Event