Lazy loading images using IntersectionObserver

Original author: Joon Park
  • Transfer
Nowadays, the main stumbling block on the way to high-speed download sites are images. This is especially true for e-commerce projects. The images on them, usually quite “heavy”, make up the bulk of the content of the pages. This, as a rule, leads to the fact that in order to show the user a page, his browser needs to load several megabytes of image data. How to speed up page loading in this situation? The answer to this question is devoted to the material, the translation of which we are publishing today.


General provisions

Consider, for example, the home page of the Home section on Walmart.

The page, which has many images

Here is information about how many images are loaded to form this page:

Images uploaded when forming the page

As you can see, there are 137 images here! This means that more than 80% of the data required for the output of the page and transmitted over the network is represented as graphic files.

Let us now analyze the network requests that are performed when the page loads:

Network requests that are made during the formation of the page

In this case, the files obtained as a result of the separation of the project code are loaded later than they could. This happens because you first need to download the main bandlcp_ny.bundle. This bundle could have been downloaded much faster if it had not been disturbed by 18 images competing with each other for bandwidth.

How to fix it? In fact, it’s not really possible to “fix” it, but you can do a lot to optimize the loading of images. There are many approaches to optimizing images used on web pages. Among them - the use of various formats of graphic files, data compression, the use of blur animation technology, the use of CDN. I would like to dwell on the so-called “lazy loading” of images (lazy loading). In particular, we will talk about how to implement this technique on React sites, but since it is based on JavaScript mechanisms, it can be integrated into any web project.

Pilot project

Let's start with this extremely simple React component Image:

  render() {
    const { src } = this.props;
    return <img align="center" src={src} />;

It accepts, as a property, a URL, and uses it to render an HTML element img. Here is the corresponding code on the JSFiddle. The following image shows the page containing this component. Please note that in order to see the image displayed by him, you need to scroll through the contents of the page.

The page with the component displaying the image

In order to implement the method of lazy loading of images in this component, you need to perform the following three steps:

  1. Do not render the image immediately after loading.
  2. Configure image appearance detection in the page content pane.
  3. Display the image after it is detected that it has fallen into the viewing area.

Let's sort these steps.

Step 1

At this step, the image is not displayed immediately after loading.

render() {
  return <img />;

Step 2

Here we customize the mechanisms that allow you to detect when the image hits the viewing area.

componentDidMount() { = new IntersectionObserver(() => {
    // тут будет код для реализации третьего шага
    root: document.querySelector(".container")
render() {
  return <img ref={el => this.element = el} />;

Let's sort this code. Here is what is done here:

  • The refimg attribute has been added to the element . This allows you to later update the link to the image without having to re-render the component.src
  • A new instance has been created IntersectionObserver(we'll talk about it below).
  • The object is IntersectionObserverasked to observe the image using the design observe(this.element).

What is it IntersectionObserver? Considering that the word “intersection” is translated as “intersection”, and “observer” is an “observer”, one can already guess the role of this object. If you look for information about it on MDN , you can find out that the Intersection Observer API allows web applications to asynchronously monitor the change in the intersection of an element with its parent or viewport document scope.

At first glance, this feature of the API may not seem very clear, but, in fact, it is very simple. The instance is IntersectionObserverpassed several parameters. In particular, we used the parameterroot, which allows you to specify the root DOM element, considered by us as a container, about the intersection of the element with the border of which we need to know. By default, this is the area in which the visible fragment of the page (viewport) is located, but I explicitly set it to use the container found in the iframeJSFiddle element . This is done in order to, later, consider one possibility that is not designed to use elements iframe.

The reason why the use IntersectionObserverto determine the moment when an element becomes visible is more popular than more traditional methods, such as joint use onScroll, getBoundingClientRect()is that the mechanisms IntersectionObserverare executed outside the main stream. However, callback called afterIntersectionObserver detects the intersection of the element with the container, runs, of course, in the main thread, so its code should not be too heavy.

Step 3

Now we need to configure the callback to be called when an element intersects target( this.elementin our case) with a container root(in our case, this is an divelement .container).

.... = new IntersectionObserver(
  entries => {
    entries.forEach(entry => {
      const { isIntersecting } = entry;
      if (isIntersecting) {
        this.element.src = this.props.src; =;
    root: document.querySelector(".container")

When the intersection is detected, a callback is passed an array of elements entriesthat resembles a set of snapshots of the state of all target elements for which an intersection of a given boundary is detected. The property isIntersectingindicates the direction of intersection. If the element for which the observation is organized falls outside the root element, it is equal true. If an element leaves the root element, then it is equal false.

So, when it turns out that the element has crossed the bottom of the container, I manually set its property srcand turn off monitoring it, which is no longer necessary.

Step 4 (secret)

Now, in the fourth, secret step of our work, you can admire the result and enjoy the success. Here is the code that contains what we just talked about.

The result of applying the technique of lazy loading of images.

However, if we take a closer look at what we did, it turns out that here you can find something not very good. In order to see this, I quickly scrolled the page, slowing down the speed of the network connection.

Behavior of the page when it is fast scrolling and slowing down the speed of the network connection.

Since we load the image only after it has reached the area in which it should already be visible, the user cannot scroll the page and see the area occupied by the image, and, of course, image itself, before it is loaded. When sites are viewed from ordinary computers connected to high-speed Internet, this does not cause problems. But many modern users visit sites from their phones, sometimes they use 3G networks or, even worse, EDGE connections.

With this problem, however, to cope is not so difficult. This can be done due to the fact that the Intersection Observer API provides the developer with the opportunity to expand or narrow the boundaries of the root element (in our case, this element .container). In order to take advantage of this opportunity, it is enough to add one line of code to where the root container is configured:

rootMargin: "0px 0px 200px 0px"

The property rootMarginshould be written a string, the structure of which corresponds to the rules of CSS used to configure the parameters of the padding elements. In our case, we inform the system that the lower boundary used to detect the intersection of an element with a container should be increased by 200 pixels. This means that the corresponding callback will be called when the element falls into an area that is 200 pixels below the lower border of the root element (the default value is 0).

Here is the code that implements this technique.

Improving the technique of lazy loading of images

As a result, it turns out that when we scroll the page only to the 4th list item, the image is loaded in an area that is 200 pixels below the visible area of ​​the page.
Now, it would seem, everything was done. But it is not.

Image height problem

If you have carefully studied the above GIF illustrations, then you may have noticed that the scrollbar performs a “jump” after the image is loaded. Fortunately, this problem is easy to handle. Its reason is that the element that displays the image initially has a height of 0, which, after loading the image, is equal to 300 pixels. Therefore, to correct the problem, it is enough to set the element to a fixed height by adding an attribute to the image height={300}.

About optimization results

What results did we, at Walmart, achieve after applying a lazy image upload on this page? In fact, specific results vary greatly depending on a variety of circumstances, including the client’s connection speed to the network, the availability of a CDN, the number of images per page, and the rules for detecting an intersection with the root element applied to them. In other words, you, in order to assess the impact of lazy loading of images on your own project, it is best to implement and test it yourself. But if it is still interesting for you to take a look at what the lazy loading of images gave us, here are a couple of Lighthouse reports. The first is formed before optimization, the second - after.

Lighthouse report generated before optimization

Lighthouse report generated after optimization


Today we looked at how to optimize web pages using lazy image loading. If the pages of your site are full of pictures, then, quite possibly, this technique is useful to you.

Dear readers! How do you optimize images and upload them?

Also popular now: