Lazy load Images using Intersection Observer API

Share this video with your friends

Send Tweet
Published 5 years ago
Updated 4 years ago

Some apps contain several images that are not visible from the beginning. When the user scrolls, then they’re visible. However, those images will be loaded by default from the beginning, lowering down the initial load.

In this lesson we’ll create a LazyImage component that uses the Intersection Observer API in order to lazy load the image when it gets into the viewport.

Instructor: [00:00] If we open the network tab in the dev tools and reload, we can see that a bunch of images are loaded at the beginning. To be precise, 50 of them, because of this limit set in the images component.

[00:17] The thing is, we are only seeing two of the images, so we don't need any more. Right now, we are loading all of them at the beginning. That can totally be the reason for a very bad performance.

[00:29] In order to solve that, let's start by creating a lazy image component, image.vue. Let's define our template, which we will copy from the images component. This is the component from [inaudible] that uses an image under the hood, and it will receive the src of the image already by property.

[00:58] Then define the script of the component with the data option, which will hold an observer instance in the [inaudible] . Then define a mounted hook where we will create the observer by calling new intersectionObserver. We need to pass a callback as its parameter, where we'll have the entries of the intersection observer.

[01:32] The entries parameter is an array of intersection entries, but we only need the first one. Let's grab it and save it in a variable. You could receive more than one if you set thresholds, but whether you use them or not, the first one will be always the first to intersect with the viewport.

[01:52] Then we can use the entry.isIntersecting property in order to check if it's intersecting with the viewport. When it intersects, we have to save it in the state of the component in order to show the real image. Let's write this intersected variable that we set to true. Let's write it within data, initialized to false.

[02:18] Finally, we have to call the observe method from the observer and we need to pass an HTML element, in this case the root of this component, which is accessible through this.$el. It's important that we use the mounted hook instead of the created because the mounted hook waits for the component to be attached to the dom, so then we have the element available in the component.

[02:44] Now let's add the prop for the src of the image, and let's write a computed property. Let's call it srcImage, which, depending on the intersected state, will return either the src of the image or an empty string. Here, in the component, instead of the src, let's use srcImage.

[03:17] We can go now to images and import the lazy image from lazyImage and add it as a component, lazyImage. Then, up in the template, instead of card-media, let's use the lazyImage that we just imported. We'll save this.

[03:54] Let's reload here, and you can see that only a few requests have been made. If we start scrolling, more images are requested and we don't have the performance problem of loading all the images at once.

[04:14] We still have an issue with the lazy image component. It is not yet optimized and it has memory leak. When it gets intersected, then the image will be shown, but the server is still working. We have to make sure that, once it intersects, we have to call the disconnect method of the observer. In that way, it will still work.

[04:40] The other case is the destroyed hook. We also have to call the disconnect method here. Otherwise, when the component is unmounted, the intersectionObserver instance is still running and checking for intersections. We still can improve it further.

[05:01] If we reload here, we can see that the new images are loaded just when they intersect in the viewport. That means that we will see an empty placeholder before it gets loaded, as you can see. What we can do is to load the image a bit before it intersects in the viewport.

[05:24] For that, the intersectionObserver constructor accepts a second argument with some options. For that, we can use the root margin, and let's say 128 pixels. This means that the intersection observer will add a margin to the element defined in the observe in order to check for the intersection.

[05:47] Right now, if we save, and if we reload again and start scrolling, you will see that the next image is intersected a bit before it enters the viewport. If you scroll smoothly, we will not see any white, empty space.

[06:05] You could adjust that root margin as you wish for your case, but right now we have an image component that is totally lazy loadable and avoids a lot of initial requests for all the images in this application.