Lazy load images with the IntersectionObserver API
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 attributelazy
: 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.

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.

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.
