Lazy loading offscreen images with plain JavaScript, Angular or React

I found one interesting suggestion by Lighthouse audit on my website, it was saying:

Consider lazy-loading offscreen images to improve page load speed and time to interactive.

And a link introduced me a relatively new IntersectionObserver API with timely advice not to observe too many things to avoid performance penalty:

Intersect all the things!
No! Bad developer! That’s not mindful usage of your user’s CPU cycles. Let’s think about an infinite scroller as an example:

Quickly I tempted to try this API on my page where a bunch of images are shown. Current image count per page is under 30 but likely to grow and 5 images pagination is already in place to keep the page loading accepably fast but I felt like user experience with prev and more buttons wasn’t that great and also user had to wait loading 5 images to see the page where user can actually see at most 2 images in a screen.

Here I will show 3 examples of using this API — first one with plain JavaScript, second one with Angular framework and last one with React library.

In all examples,

  • web served by Firebase hosting
  • fetch image URLs from the common REST API hosted on cloud function
  • server-side rendered by https cloud function
  • on client browser, use IntersectionObserver API to load only two images at a time

When using the API, my approach was quite straightforward:

  1. provide URLs for two images, [0] and [1]
  2. when image [0] is loaded, start observing it
  3. when the page scrolls down and image [0] starts to hide from its top part, stop observing it and start observing image [2]
  4. repeat steps 1~3 for [2] and [3] …

using IntersectionObserver API with plain JavaScript

In first example, the server fetches URLs and renders the page using the following Handlebars template:

Server to fetch URLs and render the page using this template

As you see, img tag doesn’t have src attribute set instead it has two custom attributes — data-src to hold URL and data-idx to hold zero-based index so when the page is initially loaded, no images will be shown yet. Let’s look at what work.js does:

Find images on page html and prepare properties

First it finds all img tags and keep them in list property and defines getter/setter for _indexToObserve property. Note that the setter sets index value and provides URLs for next two images as well.

Now it’s time to create an observer itself:

Create an observer with callback to watch scrolling down

entries will contain only one element representing img tag being observed. Among the various information given in IntersectionObserverEntry object, it sees boundingClientRect and compares its height and bottom properties to decide if the page is scrolling down. If that’s the case, it calls unobserve() function and sets the next index to observe to load more images.

The image being observed started to hide while scrolling down

Lastly it registers load event handler for all found img tags to simply call observe() when it’s loaded:

Register load events and kick-start the chain of actions

Now all set up, it sets indexToObserve to zero to trigger a chain of actions like:

  • load two images
  • when a first image loaded, start observing it
  • when scrolls down, load two more images
  • when a first image loaded, start observing it

Below are two helper functions to call the API methods — observe() and unobserve():

Helper functions — observe() and unobserve()

End result was great, it gave me a better speed index in Lighthouse audit, user wouldn’t have to touch prev or more button anymore and images just keep appearing smoothly as user scrolls down.

On most browsers on mobile and desktop, the code worked nicely but to support IE11, it was required to include a polyfill intersection-observer.js and following code as well for a missing implementation of NodeList.forEach:

A polyfill for IE11’s missing NodeList.forEach

using IntersectionObserver API with Angular framework

Second example is taking the same approach shown above but is based on Angular 4.4.6 framework so it is using Typescript and code is splitted in image.service and image.component.

image.service fetches URLs at initial page load and sets up all IntersectionObserver stuff and provides URLs. image.component emits load event then a parent component would call image.service.observe(me) in turn.

Note that image.service doesn’t handle img tag’s src attribute directly as in first example instead it just provide one additional boolean property toLoad for each image to tell image.component when to load an image. Look at the component’s template and [src] property binding:

Image component using toLoad and url properties for its src

Below image.service sets up the API:

Angular service set up IntersectionObserver

When user navigates to the page, the page component calls image.service.getUrls() to run codes below:

Fetch URLs and kick-start the chain of actions

As comment says, it fetches URLs, adds two additional properties for each image, setting first two toLoads to true to trigger the chain of actions as described in first example.

using IntersectionObserver API with React library

Third example is based on React 16 library. Similar to Angular example, code is splitted in 2 component — Observer component contains main API code providing an image list and a simpleImagescomponent just renders a given image list. Those 2 components are composed by higher-order component technique as shown below:

Work component is a HOC composed of Observer and Images

Observer component code below may look similar to image.service in Angular example except React-specific lifecycle hook methods:

Observer component handles the API and provides a list


I tried to provide 3 realistic examples that use IntersectionObserver API and hope you find this post helpful. Below are links to repositories and live demo:

Plain JavaScript example repo

Angular example repo and live demo

React example repo and live demo

Like what you read? Give Bob Lee a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.