Today images (their size and number on a single page) can be a major issue when it comes to performance. Keeping in mind that the page load of a website has a direct impact on the conversion rate, this issue should not be ignored.
In this article I would like to describe one way of reducing that initial weight of a website. What I will demonstrate is how to load only the content visible for the user when she/he sees the initial view and lazy load all the rest of the heavy-weight elements (like images) only when required.
To do that we need to solve two things:
To make it easier to understand I created an example, a list of random articles, each one containing a short description, image and a link to the source of the article. We will go through the creation process of a components for displaying that list of articles, displaying a single article and displaying (and lazy-loading) an image for a specific article.
Most of the things I would like to show are based in the component displaying the image, so I will show a basic example of how that component looks like. We will do a step by step explanation in the next sections so don’t get too deep in the code just yet.
In the component we have an
ImageSpinner component that is shown when the image is being loaded and an
img tag that is responsible for keeping the image source and displaying the image when the loading is completed.
The key lazy-load logic is extracted to a
LazyLoadDirective that is used on our component by adding
Script part of that component looks like that:
As mentioned above, the lazy-load logic is kept in a directive
This is just part of a working example, you can find the whole example in this Codesandbox example. We will analyse the code piece by piece and see what is actually happening in the next section.
Let’s start with creating a component that will show an image (no lazy-loading involved). In the template, we create a
figure tag that contains our image, the image itself receives
src attribute that carries the image source (url).
In the script part we receive the prop
source that gives us source url of the image we are displaying.
That is fine, we render the image we wanted, but if we leave it as it is, we will be loading image straight away without waiting for the component to be visible on our screen. That is not what we want so let’s go to the next step.
To prevent the image from being loaded we need to get rid of the
src attribute from the
img tag. But as pointed out at the beginning we still need to keep the image source somewhere. Good place to keep that information is data-* attribute.
Sounds like a perfect fit for our need.
Ok, with that done we will not load our image. But wait, we will not load our image… ever!
Obviously that is not what we wanted, we want to load our image but under specific conditions. We can request the image to be loaded by replacing the
src attribute with the image source url kept in
data-url. That is the easy part, the issue we encounter now is - when should we replace that
We would like to load the image when the component carrying it becomes visible to the user. How can we detect if the user sees our image or not? Let’s check that out in the next step.
This very inefficient way of detecting if an element is visible in the viewport can be solved by using Intersection Observer API.
Looking at the definition it allows you to configure a callback that is called whenever one element, called the target, intersects either the device viewport or a specified element.
Firing a custom callback function when the element becomes visible in the viewport? Sounds like a magic spell for what we need.
So, what we need to do to use it?
To use the Intersection Observer we need to do few things:
What is a custom directive? As per documentation, it is a way to get the low-level DOM access on elements. For example changing an attribute of a specific DOM element, in our case changing
src attribute of an
Our directive looks like below. Again, we will break it into pieces in a moment, I just want to give you an overview.
Let’s go step by step.
insertedhook because it is called when the bound element has been inserted into its parent node (this guarantees parent node presence). Since we want to observe visibility of an element in relation to its parent (or any ancestor), we need to use that hook.
el, element on which the directive is applied. We can extract the
imgfrom that element.
imgelement with the source url of the image we want to request and show (that triggers the request).
loadImageon certain conditions.
entrieswhich is an Array of all elements that are watched by the observer and
entriesand check if a single entry becomes visible to our user
isIntersecting, if it does
loadImagefunction is fired
unobservethe element (remove it from the observer’s watch list), that prevents the image from being loaded again.
thresholdand options object that carries our observer options.
rootthat is our reference object on which we base the visibility of watched element (it might be any ancestor of the object or our browser viewport if we pass
null). It also specifies
thresholdvalue which can vary from 0 to 1 and tells us at what percentage of the target’s visibility the observer’s callback should be executed (0 meaning as soon as even one pixel is visible, 1 meaning the whole element must be visible).
Even though it is not supported by all browsers, the coverage of 73% of users (as of 28th August 2018) sounds good enough.
But having in mind that we want to show images to all users (remember that using
data-url prevents the image from being loaded), we need to add one more piece to our directive.
We need to check if the browser supports IntersectionObserver, fire the
loadImage if it doesn’t (that will request all images at once) and
createObserver if it does.
To use our newly created directive we need to register it first. We can do it in two ways, globally (available everywhere in the app) or locally (on a specified component level).
To register a directive globally we import our directive and use
Vue.directive method passing the name on which we want to register our directive and directive itself. That allows us to add
v-lazyload attribute to any element in our code.
If we want to use our directive only in a specific component and restrict the access to it, we can register the directive locally. To do that we need to import the directive inside the component that will use it and register it in the
directives object. That will give us the ability to add
v-lazyload attribute to any element in that component.
After registering our directive we can use it by adding the
v-lazyload attribute to the parent element that carries our
figure tag in our case).
Lazy Loading images can significantly improve your page performance. It allows you to load the images only when the user can actually see them.
For those still not convinced if it is worth playing with it I prepared some raw numbers. Let’s take our simple articles list. At the moment I was performing that test it had 11 articles with images (meaning 11 images on the page). I don’t think that is a lot of images, you can probably find bigger number in a second by going into any news page.
Let’s stick to our 11 images and check the performance of our page on fast 3G with only first article visible, without lazy-loaded images.
As expected 11 images, 11 requests, total page size 3.2 MB.
Now, the same page with only first article visible and lazy-loded images.
Result, 1 image, 1 request, total page size 1.4 MB.
By just adding this directive to our articles we saved 10 requests and we reduced the page size by 56% and bear in mind this is really simple example.
No more comments, let the numbers speak for themselves.