DEV Community

Abdullahi Hamisu
Abdullahi Hamisu

Posted on

Intersection Observer API: A tool for optimizing Infinite Scroll

Background

I remember my first experience with infinite scroll on Facebook’s mobile app and how it significantly improved the user experience. For context, I have had to deal with slow internet, and the idea of having to wait for the browser to load a new page to access the next page was frustrating even for users with reliable connections.

The first resort for any developer is to set up an event handler that fires on scroll and triggers a function that makes an API call to fetch the next data as the scrollbar approaches the end of the scroll track.

This implementation works, but it's far from perfect, and this is where the Intersection Observer API shines, from infinite scroll, and lazy loading to complex scroll-based animations and storytelling —-you name it.

The Intersection Observer API

The Intersection Observer API is a browser feature that enables developers to detect when elements enter into the viewing area of a scrollable element.

Intersection is better explained in relation to a scrollable parent element, which is known as the root element.

Imagine a scrollable parent element with child elements. The child elements that are visible initially without scrolling are said to be intersecting the parent element. The other child elements, which are not initially visible, are considered intersecting only when the parent is scrolled to bring those child elements into view.

Why the Intersection Observer API?

As stated, traditional scroll animations and infinite scrolling relied on listening for the scroll event and triggering event handlers when the target element entered into view.

The downside to this working but not ideal implementation is that the browser calls the event handler function whenever the page or root element is scrolled. Even the smallest change in scroll will trigger the event handler to run. These function calls overload the browser’s memory and lead to overall poor performance.

Think of it this way. With every scroll, the browser repeatedly calls the same event handler function until eventually the target element is reached, which is usually the bottom-most element in the case of infinite scroll. You can only imagine how many of these function calls are made, and it’s no wonder why this implementation leads to performance issues.

With the Intersection Observer API, the callback function runs only once.

Scroll events vs. Intersection Observer

Here we take an evidence-based approach to compare the scroll-based implementation and the intersection observer API.

Scroll-based implementation of infinite scroll

The code below describes a page containing divs with some stylings. An event listener has been attached to the body element to execute a callback function whenever the container is scrolled.

This practical implementation may seem okay from the start, but not until you begin to scroll the page do you begin to notice just how many of those function calls are made.

From the attached image below, you can see that we’ve made a total of 32 function calls, and this is just by scrolling roughly halfway and not all the way through to the end of the page.

scroll triggers vs intersection observer

Now imagine if this callback function were to contain logical computations for animations or any other compute-intensive feature. With the Intersection Observer API, these excessive function calls are replaced with a single function call no matter the number of times a page is scrolled.

Advantages of Intersection Observer API over scroll-based event triggers

We’ve discussed performance as one of the main benefits offered by intersection observer implementations; however, the benefits go beyond performance benefits.

The following are some of the benefits of intersection observer API implementations over scroll-based event triggers.

Cleaner code

Much more code is written in scroll-based implementation, which may include targeting logic in the case of infinite scroll. This makes the codebase much more difficult to understand compared to Intersection Observer, where some aspects like element targeting are automatically handled, resulting in clean code.

Automatic element targeting

Intersection Observer was built to target elements when they enter into the viewport. Therefore, there is built-in logic to target elements without the need to implement complex logic, which might bloat the codebase and introduce memory and performance issues.

This automatic handling of element detection shifts the focus to other critical aspects like the callback functions as opposed to scroll-based implementations, where both element targeting and callbacks become the sole responsibility of the developer.

Productivity

Features like infinite scroll and scroll animations are easily implemented with the Intersection Observer API in a much shorter timeframe than scroll-based implementations, saving time and improving efficiency for developers.

Use cases of the Intersection Observer API

The Intersection Observer API has lots of use cases, from infinite scrolling, scroll animations, and lazy loading to not-so-common implementations like scroll-based form validation, as noticed on Discord, where scrolling to the bottom of a modal component enables a button that had been previously disabled.

Anatomy of the Intersection Observer API

The IntersectionObserver interface takes in two parameters during instantiation; these are

Options

The options parameter is a JavaScript object that determines the root element and how the target element is to be observed.

const options = {
       root: document.getElementById("body"),
       rootMargin: "0px 0px 0px 0px",
       threshold: 1.0
}
Enter fullscreen mode Exit fullscreen mode

Anatomy of an options parameter

There are 3 key properties for the options property.

root

The root property accepts an Element object, which serves as the parent of the child elements to be observed.

The root element’s area will now serve as the reference area that determines whether a child element is intersecting or not.

If the root element is not specified, the browser will automatically use the body as the root element.

rootMargin

This parameter accepts a set of four values similar to the margin property in CSS.

The four values correspond to the top, right, bottom, and left margins, respectively.

A positive margin value will increase the bounding area of the root element, and a negative value will decrease or shrink the size of the bounding area.

The default value is 0px for all four directions, meaning the container stays as is.

threshold

Threshold determines the percentage level of visibility of the target element at which the callback function is executed.

The property takes either a single number ranging from 0 to 1 or an array of numbers, each number also being within the range of 0 to 1.

A threshold of 0.5 means that the callback function will be executed when 50% of the element intersects the root element, which also means half of the element is now visible in the root element.

A threshold of 1 means 100% of the element needs to be visible, at which point the callback is executed.

If an array of numbers is passed as the threshold, the callback function will run at each of the thresholds in the array.

Example: given a threshold of [0.3, 0.6, 1], the callback function will run successively each time the target element intersects its parent at 30%, 60%, and 100%.

If the threshold is not specified, the browser will use 0 as the default, and the callback function will run when the smallest part of the child element intersects its parent.

Callback

The callback parameter is the function that is executed when the target element intersects the root element at the specified threshold.

It is a normal JavaScript function that takes a single parameter, entries.

const callback = (entries) => {
       //entries
}
Enter fullscreen mode Exit fullscreen mode

So, what are entries?

Entries

Entries is an array of information about all the elements observed by the intersection observer. Entries stores a collection of IntersectionObserverEntry objects, which represent information about the elements monitored for an intersection.

Below are some of the properties of the IntersectionObserverEntry object.

isIntersecting

This property tells us whether a target is intersecting its root element or not. It returns either true or false.

target

This property returns the HTMLElement object of the target that’s being observed.

Implementing the Intersection Observer API

Having discussed the necessary details, it’s time to implement the Intersection Observer.

A basic implementation of the Intersection observer has been discussed below.

Simple scroll animation

This section shows the practical implementation of the Intersection Observer API for a simple scroll animation.

Aim

A page containing 6 div elements with a class of box has been set up. The aim is to change the background color of each box and move it to the right by 20% whenever it intersects with the viewport.

The code below represents the default styling for the div elements.

Creating the options object

We want our div elements to be fully visible before we change their background color; therefore, the threshold property of the options objects will be set to 1.

We do not want to change the size of the viewport, so the rootMargin property will be set to the default value of 0px for all four sides.

Finally, we set the root property to be the body element; that way, our divs change color when the page is scrolled.

const options = {
       root: document.getElementById("body"),
       rootMargin: "0px 0px 0px 0px",
       threshold: 1.0
}
Enter fullscreen mode Exit fullscreen mode

Creating the callback function

The callback function is the heart of the intersection observer, as it determines what happens when elements intersect their root element.

As stated above, we want to change the background color and move our target div by 20% whenever it intersects the root element 100%. Luckily, there are built-in functions that assess whether a target is intersecting the root element or not.

Since the callback function takes in an entries parameter, which is just an array of the elements being observed, we want to loop through each entry and check if the target is intersecting the root element by calling its isIntersecting property. If the target happens to be intersecting, then we call its target property to assess its Element object and add the styling class.

The implementation can be found below.

const callback = (entries) => {
    entries.forEach((entry) => {
        if(entry.isIntersecting){
            console.log("intersecting");
            entry.target.setAttribute("active", "active");
            console.log("target ", entry.target.classList)
        } 
        if (!entry.isIntersecting){
            entry.target.removeAttribute("active");
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together

Now that we have an options object and a callback function. All that’s left is to instantiate the IntersectionObserver interface and pass in the arguments and call.

const observer = new IntersectionObserver(callback, options);
Enter fullscreen mode Exit fullscreen mode

There’s still one final piece left. To observe an element, you call the observe method of the IntersectionObserver object and pass the target element as an argument.

Since we’re watching several divs with a common class identifier, a better approach is to iterate through an array containing their objects and pass it to the observe method as an argument.

const arr = document.getElementsByClassName("box");

for(let item of arr){
    observer.observe(item);
}
Enter fullscreen mode Exit fullscreen mode

Infinite Scroll

Aim

With Infinite scroll we want to load the next data whenever the the last item in the list intersects with the root element. After loading the new elements to the DOM, we ant to unobserve the previous last item on the list that served as the trigger for loading the new data, and we want to observe the last item from the recently loaded data to serve as the trigger.

<div id="root">
  <div class="box">1</div>
  <div class="box">2</div>
  <div class="box">3</div>
  <div class="box">4</div>
  <div class="box">5</div>
  <div class="box">6</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Implementing the options object

Since we want our list to be confined to a specified scrollable container, we want to set it as our root element and set the threshold to 1 to make sure the page trigger is fully visible before loading the next data.

const options = {
  root,
  threshold: 1.0,
}
Enter fullscreen mode Exit fullscreen mode

Implementing the callback

In the callback function, we check for the trigger to intersect the root element. If that happens, we load a the next data.

To prevent loading same data multiple times, we create a loading variable and set it to false initially.

During data loading, we set the loading variable to true.

This way, we prevent loading the same dat multiple times.

const callback = (entries, observer) => {
  target = entries[0];
  if(target.isIntersecting && !loading){
    //load new data
    loading = true
    let newData;
    console.log("wait");
    setTimeout(()=>{
      newData = loadData(current);
      root.appendChild(newData);
      loading = false
      console.log("finished")
    }, 5000);
    observer.disconnect();
   current+=6;
   observer.observe(root.lastElementChild);
  }
}
Enter fullscreen mode Exit fullscreen mode

Complete implementation

Below is the complete JavaScript implementation of infinite scroll with the Intersection Observer API.

let current = 6;
let loading = false;
const root= document.getElementById("root");

const options = {
  root,
  threshold: 1.0,
}

const callback = (entries, observer) => {
  target = entries[0];
  if(target.isIntersecting && !loading){
    //load new data
    loading = true
    let newData;
    console.log("wait");
    setTimeout(()=>{
      newData = loadData(current);
      root.appendChild(newData);
      loading = false
      console.log("finished")
    }, 5000);
    observer.disconnect();
   current+=6; observer.observe(root.lastElementChild);
  }
}

const loadData = (start) => {
  const fragment = document.createDocumentFragment();
  for(let i=start+1;i<=start+6;i++){
    const newElem = document.createElement("box");
    newElem.classList.add("box");
    newElem.textContent = i.toString();
    fragment.appendChild(newElem);
  }
  return fragment;
}

const observer = new IntersectionObserver(callback, options);
observer.observe(root.lastElementChild);
Enter fullscreen mode Exit fullscreen mode

Pitfalls

When using the intersection observer API in frameworks like React, Vue, and Angular, it is usual for components to be re-rendered. If those components use the Intersection Observer API, it is important to unobserve all the observed elements before the component is removed from the DOM to prevent the introduction of memory leaks.

This can be accomplished by either calling the unobserving elements individually using the unobserved method or calling the disconnect method to unobserve all elements from the intersection observer in one go.

observer.disconnect();
Enter fullscreen mode Exit fullscreen mode

Conclusion

This wraps up the article on Intersection Observer. Implementations like lazy loading and would have been featured here, but at the cost of making the article too long; we restrict ourselves to the basic implementation to serve as a building block.

However, links have been shared below to resources on infinite scroll with intersection observer as well as further readings on the intersection observer API.

Futher Readings

The rootMargin property

Intersection Observer Entry

Top comments (0)