Intersection Observer API in Action

Shiksha Engineering
9 min readMay 7, 2020

--

Author: Virender Pratap Singh

What is Intersection Observer API

The abstract of the W3C public working draft describes the Intersection Observer API as:

This specification describes an API that can be used to understand the visibility and position of DOM elements (“targets”) relative to a containing element or to the top-level viewport (“root”). The position is delivered asynchronously and is useful for understanding the visibility of elements and implementing pre-loading and deferred loading of DOM content.

Why Intersection Observer API?

There are a few key features of this API, listed below:

  • It does not work on the main execution thread, but rather works in the background.
  • It provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element (or the viewport).
  • Provides a callback function, called whenever the target element intersects with the root element (or the viewport).
  • Click here to get more details about Intersection Observer API.
  • Check the official W3C documentation here.

How is it compared to Scroll Event Listener

Comparison Scenario — 1

  1. The first test has 1 observer or 1 scroll event with one callback each.
  2. The second test has 100 observer or 100 scroll event with one callback each.
  3. The third test has 100 observer or 100 scroll event with 100 callback each.

Comparison Scenario — 2

  1. With no offset caching, no callback throttling

2. With offset caching, no callback throttling

3. With offset caching, callback throttling

4. With Intersection Observer

Explanation of above graphs:

  1. The yellow graph means computational stress. The higher, the more work the main thread has to do.
  2. The green graph above the yellow are the FPS. The higher & steadier it is, the better experience the user will have.
  3. The small red dots above the green graph mean that the callback time extended beyond one Event loop. The less they appear the better.

If we accumulate all the “yellow” time and divide it by the total time, we get the percentage of time spent doing scripting work on the main thread:

Percentage of total time doing scripting work with 4x CPU slowdown

Scroll Listener — No Caching & No Throttling: 48.9%

Scroll Listener — Caching & No Throttling: 43.5%

Scroll Listener — Caching & Throttling: 28.9% (Most optimized)

Intersection Observer: 23.3%

Percentage of total time doing scripting work with 6x CPU slowdown

Scroll Listener — Caching & Throttling: 63.0%

Intersection Observer: 37.6%

Our Two Use Cases Solved Using Intersection Observer API

  1. Image Lazy Loading : We have a Web app in React where we frequently show content posted from TinyMCE editor. This content includes plain HTML with tables, iframes, images, paragraphs etc. and saved directly into DB. This content is too large to be processed at the front-end hence it is served using dangerouslySetInnerHTML in React. In many cases, this content contains images and has to be loaded lazily. Lazy loading images with a React Component was easy but with the HTML set using dangerouslySetInnerHTML, this was a little bit tricky. So, the Intersection Observer API comes as a saviour in this case.
  2. Infinite Scrollable Page : In the second scenario, we need to build a web page which is scrolled infinitely, i.e. more and more content is loaded as the user reads through the page. When the user is towards the end of page, we load the next page in the background and the user is shown the next page and the URL is also replaced by the URL of the next page. Here, we could implement this by binding scroll events but the Intersection Observer API provides a clean and better solution to this problem.

1. Lazy Loading Images with Intersection Observer API

In our React app, I have created a utility to Create and Destroy the Observer and attach the Observer to Image targets.

Creating an Observer Instance First, I created a config and a callback function to initialise our Observer. Below is the config:

config = {
root: null, /* wrt viewport */
rootMargin: "0px", /* no margin around the viewport */
threshold: [0] /* when to fire the callback */
};

window.observer = new IntersectionObserver(handleIntersectCallback, config);

The creation of the Observer is placed in componentDidMount lifecycle method.

Destroying the Observer Instance Then I created a function to destroy the Observer and placed its call in componentWillUnmount lifecycle method.

window.observer.disconnect();

Observing the Intersection Lastly, I attached each target (img tags with lazy class) with the Observer’s instance.

window.observer.observe(imgTag);

The attachment of each <img> tag with Observer is placed in componentDidMount (for initial load) and componentDidUpdate (for subsequent updates after re-render) lifecycle methods.

Now, whenever an <img> tag is intersected with the viewport’s border, the callback function is called. In this case the callback function is handleIntersectCallback.

Note: It is to be noted that this callback function is executed on the main thread. Any heavy operation here might affect the performance of the page.

Executing the callback Now, when the callback is called, we are provided with an array of all the intersecting <img> tags. Using this array, we will replace the src attribute of <img> tag and achieve the lazy loading of images.

2. Infinite Page Scroll with Intersection Observer API

Infinite scrolling is when a user is reading the content on a web page and when is about to reach the end, the next page is loaded in the background. When the end of the web page is reached, the Intersection Observer fires our callback to load the next page.

Generally the behaviour of infinite loading is applied with Pagination of pages, but our use case is slightly different. With Pagination, only the next set of records are loaded when the page is reached to the bottom. This is quite simple and implemented a thousand times.

In our use case, the next page is not a set of records of the next page, but it is a whole new page. With the new page comes new content, both primary and secondary content. Here primary content is the main content of the page while the secondary content is some related widgets on the page, which changes with every page. Plus, if any thing runs in the background, it should be loaded too.

But before implementing the Infinite Scrolling in our page, it must be provided with some pre-requisites.

a) Setting up the DOM

To get the infinite scroll behaviour, we need to set our page with some specific DOM elements and cut the HTML Views accordingly.

b) Loading the next page in the background

After loading the page, prepare an initial config in a JS object. The config is shown below :

var initialConfig = {
currEntityId : 1234, -> entity id of current page, initially first page
placeHolderTag : ‘blgHd-’, -> id prefix of element on which Observers are attached
scrollDir : null, -> scroll up/down
noOfScreenScrollRemains : 2, -> position on which next page call is made
entityStack : [1234], ->maintain a stack to track pages, initially with first page
scrollContentIds : [2345, 3456, 4567, 5678, 6789], -> list of page IDs to be loaded
scrollContentUrl : [‘url2345’, ‘url3456’, ‘url4567’, ‘url5678’, ‘url6789’], ->urls of subsequent pages
customAjaxCallback : ‘nextPageLoadedCallback’, ->
nextPageCallback : ‘prepareNextPageCallback’, ->
appendSelectorTag : ‘.allPageContainer’, -> container in which subsequent pages are appended
};

On page load, create an Observer by passing the below config :

var options = {
root: null, /* wrt viewport */
rootMargin : "0px", /* no margin around the viewport */
threshold: [0.5] /* callback fired when 50% of target (i.e. #blgHd-1234) is visible */
};

window.infiniteScrollObserver = new IntersectionObserver(handleIntersectCallback, options);

Then attach the Observer (i.e. infiniteScrollObserver) with our first page.

attachEventToObserver(placeHolderTag + currEntityId); // refer initialConfig

I also attached a listener to check whether the scroll has reached the Footer section minus some buffer space which is calculated using initialConfig’s noOfScreenScrollRemains. So, the Ajax call to load the next page is made after some scrolls are left to reach the Footer (This enhances the user experience).

Once this position is reached, I pop the initialConfig’s scrollContentIds array to get the next page ID. And its URL is taken from initialConfig’s scrollContentUrl. An Ajax call is made to load the next page into the DOM. The HTML of next page is appended in initialConfig’s appendSelectorTag. And the Observer is also attached to this page. Loading the next page on demand (through Ajax) and at the correct moment is done. Now comes the important part of attaching each page which is loaded dynamically, to the Observer.

attachEventToObserver(next page Id); // next page Id was popped from initialConfig’s scrollContentIds array.

The pages are loaded until the initialConfig’s scrollContentIds array is empty. This array can also be pushed with new values to achieve the infinite scroll behaviour. Otherwise, if the array got empty, then no new page will be added.

c) Intersection Observer in Action

Now the Intersection Observer comes into action. Each time an element (i.e. blgHd-xxxx) intersects the viewport (called an Entry), the Observer’s callback is called. Remember that no heavy operation is to be done in this callback as it executes on the main thread.

Now, whenever an Entry comes into viewport, we need to execute some code which is specific to that page, like changing the URL, updating the page’s SEO, preparing new parameters for any kind of tracking.

Each Entry describes an intersection change for one observed target element and has some properties attached to it.

entry.boundingClientRect
entry.intersectionRatio
entry.intersectionRect
entry.isIntersecting
entry.rootBounds
entry.target
entry.time

2 of the above properties are used in our case.

isIntersecting is a flag which is true if the element’s visibility has passed the threshold value (i.e 0.5 in this case) in the Observer’s config. intersectionRatio is the current value of how much part of the element is shown in the viewport.

When isIntersecting property is true, intersectionRatio value is greater than the threshold value and scroll direction is downwards, then :

  1. Push the Entry’s Page ID to the stack (i.e. initialConfig’s entityStack)
  2. Add URL to the Browser’s History
  3. Execute the callback i.e. initialConfig’s nextPageCallback

Or, when the scroll direction is upwards and intersectionRatio is less than the threshold value, then :

  1. Fire history.back(), and listen for popstate event
  2. On popstate event, pop the Entry’s Page ID from the stack (i.e. initialConfig’s entityStack)

Challenges faced

The Intersection API is a great API but I faced a couple of things not directly related to the API.

  1. One problem was to load fresh parameters for page’s Beacon, GTM and DFP banner and initiate new calls based on new parameters. To solve this, parameters are saved in JavaScrpt’s global variables. Whenever a new page is loaded, these variables are updated and fresh calls are made.
  2. When the content is loaded, the URLs are updated the Browser’s history. Let’s say the user reached the 4th page and refreshed the page, what will happen now? Now the starting URL is the 4th page, but some flickering is observed on the page. The browser’s default behaviour is to maintain the scroll positions when user navigates to different pages on the website. To solve this, I used scrollRestoration property of History interface. Its default value is auto, I set this to manual to avoid the scroll restoration and hence the flickering on the page.

Scenarios where Intersection Observer API can not be used

  • Some cases where target element has height greater than the viewport

References

New in Intersection Observer API

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response