Quick look into the Intersection Observer API

Joel Oliveira
Joel Oliveira
Apr 22 2022
Posted in Engineering & Technology

A modern way of handling an element's visibility

Quick look into the Intersection Observer API

If you are familiar with modern Javascript APIs, you probably already discovered that a reasonable amount of Observer APIs are now available. Things like the MutationObserver, PerformanceObserver, ResizeObserver and the one we are talking about in this post, the IntersectionObserver, can now be used in all major browsers. These can help you handle a variety of use cases, that otherwise would need event handlers and sluggish routines, that run on the main thread, and potentially cause performance issues.

For example, a very common use case would be an infinite scroll, where new content is loaded as the user scrolls to the bottom of a page. In the past, you would need to use the window scroll event listener and calculate if the user is at the bottom of the page to load more content. As the user scrolls the page, these detection routines are firing constantly, affecting considerably the performance of a page, resulting in an poor experience.

The Intersection Observer API lets code register a callback function that is executed whenever an element enter or exits the viewport (or another element) or when the amount by which the two intersect changes by a requested value. This way, the website no longer needs to implement any main thread work to check for these detections and the browser will optimize this for you.

Availability

At the time of writing, this API is available in all major browsers, as you can see in the table below:

But you can always check for availability using the following check:

if ('IntersectionObserver' in window) {
    //OK, you can use it
}

Usage

But let's take a look on how you can then use this API. Basically, you invoke the IntersectionObserver using the following method:

let observer = new IntersectionObserver(callback, options);

This method's constructor takes two parameters. The first one is a callback function that will be executed once the observer detects an intersection which will include some data about that intersection. And the second (which is optional) is an options object, which can define what's going to be the intersection.

Options have 3 properties:

root

The ancestor element/viewport that the observed element will intersect.

rootMargin

A perimeter of the root element, shrinking or growing the root element’s area to watch out for. It’s similar to the CSS margin property.

threshold

An array of floating points (between 0 and 1), each representing the distance an element has intersected into or crossed over in the root and at which the callback is triggered.

But let's dive into some practical examples that will better explain how all this works.

Lazy loading images

This is a very common use case, basically you initially load all the elements with a very small placeholder image and only when the element becomes visible, you will load the actual image. Consider a list of images in a page, as follows:

<div class="gallery">
    <img src="placeholder.png" data-src="/images/image1.jpg">
    <img src="placeholder.png" data-src="/images/image2.jpg">
    <img src="placeholder.png" data-src="/images/image3.jpg">
    <img src="placeholder.png" data-src="/images/image4.jpg">
    <img src="placeholder.png" data-src="/images/image5.jpg">
</div>

Basically, when the page first loads, it will create the elements and load the same placeholder image, we will use the data-src attribute to hold the information about the file we want to load as soon as the image becomes visible.

With the IntersectionObserver, you can do this by implementing the following:

const options = { rootMargin: '0px 0px -100px 0px' };
let observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
    entry.target.src = entry.target.dataset.src;
    observer.unobserve(entry.target);
  });
}, options);
document.querySelectorAll('.gallery > img').forEach(img => { observer.observe(img) });

As you can see, in this example we will observe all images inside the div with the gallery class and instruct the observer to trigger the callback function as soon as an image is 100px above the viewport. Finally, we will load the actually image using the value in the data attribute. We will also remove that observer as soon as that happens.

Auto-Pause Videos

Another common use case for the IntersectionObserver would be to pause/play a video whenever that element is visible or not. This will make sure that the video is paused if you scroll the page to read some other information and resumes whenever you scroll back to the video.

To achieve that, we would probably have a video element as follows:

<video src="myVideo.mp4" controls autoplay></video>

Now, consider the following code:

let video = document.querySelector('video');
let isPaused = false;
const options = { threshold: 1 };
let observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if ( entry.intersectionRatio !== 1  && !video.paused ) {
      video.pause();
      isPaused = true;
    } else if (isPaused) {
      video.play();
      isPaused = false;
    }
  });
}, options);
observer.observe(video);

Basically, with this code, we will instruct the observer to trigger the callback function as soon as the video is fully visible, in the viewport (note the threshold: 1), pause the video if it's not and resume as soon as it is. Simple, right?

Infinite Scroll

Finally let's take a look at the most common use case for the IntersectionObserver, the infinite scroll. For this example, let's consider the following markup:

<ul class="articles">

</ul>
<div class="loader">Loading more articles...</div>

When the page first loads, we will populate the element with the articles class with some initial items and load more items whenever we scroll down to the element with the loader class:

let articles = document.querySelector('.articles');
let loader = document.querySelector('.loader');
let page = 1;
let items = [];
const options = { threshold: 1 };

let observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if(entry.isIntersecting){
      load(page);
    }
  });
}, options);

async function load(batch) {
   let results = await fetchArticles(batch); // pseudo function that fetches articles
   results.articles.forEach((article) => {
     let li = document.createElement("li");
     li.appendChild(document.createTextNode(article));
     articles.appendChild(li);
     items.push(article);
   });
   if (items.length === results.total) {
     observer.unobserve(loader);
     loader.hide();
   } else {
     page++;
     loader.show();
   }
}

window.addEventListener('DOMContentLoaded', function() {
    load(page);
});

observer.observe(loader);

Ready to observe along?

As you can see, with the Intersection Observer API, you can considerably simplify how you achieve rather complex tasks, without giving up on the performance of your pages. And these examples only cover some of the options available in this API, for more in-depth information, please take a look at this page.

As always, we hope you liked this article and if you have anything to add, maybe you are suited for a Developer position in Notificare. We are currently looking for a Core API Developer, check out the job description. If modern Javascript is your thing, don't hesitate to apply!

Keep up-to-date with the latest news