The Intersection Observer: The Key to Faster Element Tracking by Scrolling

Hamid Reza Salimian
8 min readApr 6, 2024

Before describing it, I want to mention some real examples and features that can be handled by an Intersection Observer:

1. Lazy-Loading of Resources

ne of the most impactful uses of the Intersection Observer API is for lazy-loading images, videos, and other resources. By only loading content when it’s about to enter the viewport, websites can significantly reduce initial load times, save bandwidth, and improve the user experience on both desktop and mobile. This approach ensures that users are only downloading what they see, making web pages faster and more responsive.

2. Infinite Scrolling

Infinite scrolling enhances user engagement by continuously loading content as the user scrolls down, eliminating the need for pagination. The Intersection Observer API can efficiently manage this by detecting when the user is close to the bottom of the content, triggering the loading of additional content just in time. This mechanism is ideal for social media feeds, news sites, and image galleries, providing an uninterrupted browsing experience

3. Advertisement Visibility Tracking

Advertisement revenue models often depend on the visibility of ads on the page. Intersection Observer enables precise tracking of advertisement visibility, helping publishers to accurately report viewability metrics to advertisers. This not only ensures transparency but also enables optimization of ad placements for better visibility, directly impacting ad performance and revenues.

4. Conditional Animations and Tasks

Intersection Observer can dynamically manage animations and tasks based on element visibility. This capability allows developers to create interactive web pages that respond as users scroll. Whether it’s triggering animations, starting videos, or pausing background processes for off-screen elements.

5. Accessibility Improvements: Dynamically manage focus inputs or content as users navigate through content, improving site accessibility.

6.Resource Prioritization: Adjust the loading priority of off-screen images or content based on user behavior and connection speed.

7.User Behavior Analytics: Gain insights into how users interact with a webpage by tracking which elements are viewed most frequently and for the longest duration.

8.Interactive Elements Activation: Automatically activate interactive elements, such as chatbots or help buttons, when they become visible.

I’ve put sample codes all above in HERE

So now you know how important is it, let’s see how can we get them …

Intersection Observer in a nutshell

The Intersection Observer API provides an efficient way to detect when an element becomes visible or intersects with another element or the viewport. In simpler terms, it lets you know when an object comes into view or leaves it, without the need for continuous scrolling monitoring or complex calculations.

Example: Adding Animation Classes with Intersection Observer

Suppose you have several elements you want to animate on your webpage:

<div data-on-reach="animate fadeInUp">Content 1</div>
<div data-on-reach="animate fadeInLeft">Content 2</div>
<div data-on-reach="animate fadeInRight">Content 3</div>
// Define the Intersection Observer
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Get the animation classes from the `data-on-reach` attribute.
const newClassNames = entry.target.getAttribute('data-on-reach');
// Split the classes and add them to the element.
newClassNames.split(' ').forEach(className => {
entry.target.classList.add(className);
});
// Optionally, stop observing the element to improve performance.
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '0px', // Trigger as soon as the element comes into view.
threshold: 0 // Even a pixel of visibility triggers the callback.
});

// Observe elements with the `data-on-reach` attribute.
document.querySelectorAll('[data-on-reach]').forEach(target => {
observer.observe(target);
});
  • 1) Setting Up the Observer: An IntersectionObserver is instantiated with a callback function. This function is executed for each observed element as it enters or exits the intersection with the viewport (or a specified root element).
  • 2) Callback Function Logic: For each element that becomes visible (isIntersecting is true), the callback retrieves the animation classes from the element's data-on-reach attribute. It then adds each class to the element, triggering the CSS animations. After adding the classes,
  • 3) Observation Setup: All elements marked with the data-on-reach attribute are selected and passed to the observer for monitoring. As these elements enter the viewport, their respective animations are triggered, creating a dynamic, engaging user experience as content becomes visible.

When the callback will be called?

The IntersectionObserver function, known as the callback function, is called not only when you scroll and an observed element reaches the viewport, but also under a few other conditions:

  1. When an observed element enters the viewport: This is the most common trigger. As you scroll through a webpage, any observed element that comes into the viewport (or specified root element) boundary based on the observer’s threshold will trigger the callback function.
  2. When an observed element leaves the viewport: If you’re observing an element that is already in view and then scrolls out of view, the callback function can be triggered again, indicating the element is no longer visible according to the specified thresholds.
  3. When the page loads: If an observed element is already within the viewport when the page finishes loading, the callback function will be triggered. This ensures that elements visible at page load can be handled immediately, without needing to wait for a scroll event.
  4. On changes to an observed element’s visibility due to layout changes: If something happens on the page that changes the layout and causes an observed element to either enter or exit the viewport (like changing the size of elements, adding new content that pushes things around, etc.), the callback can be triggered. This is because the IntersectionObserver is effectively watching for any change in the intersection status between the observed elements and the viewport/root element.
  5. Initially observing the element: When you first call observer.observe(targetElement) to start observing an element, the callback function will run if the element meets the visibility criteria defined in the IntersectionObserver options. This initial check allows you to handle elements that should be immediately acted upon without waiting for a user interaction like scrolling.

For example, If the user reloads the page and an observed element is already in view, the callback function will trigger, ensuring the animation runs without requiring the user to scroll. Similarly, if layout changes cause an observed element to become visible or hidden, the callback ensures your application can respond to these changes in real-time.

Negative or positive `rootMargin` ?

The rootMargin option in the Intersection Observer API is akin to the CSS margin property. It allows you to specify a set of margins around the root element, effectively expanding or contracting the area within which the observer checks for intersections with the target elements. This is specified as a string with one to four components, each ending in px or % (just like CSS margins), in the format "top right bottom left".

Suppose you want the content to start loading 100 pixels before it scrolls into view. You could set rootMargin as follows:

const observer = new IntersectionObserver(callback, {
rootMargin: '100px 0px 100px 0px', // 100px margin at the top and bottom
});
  • Preloading Content (+): By setting a positive rootMargin, you can ensure that content starts loading or animating before it actually enters the viewport, enhancing user experience by making content appear seamless and immediate.
  • Reducing Load (-): Conversely, a negative rootMargin can be used to delay loading until elements are closer to the viewport, which can be useful for conserving bandwidth and reducing load on the brow

Traditional Method: a listener and Using Element.getBoundingClientRect()

Why this method is very interesting to me? it’s supper supper faster than the Traditional one which I used, so I would say… what a silly way I’ve used … why Hamid !? :))

Before the Intersection Observer API, a common approach to detect if an element was in the viewport was to use Element.getBoundingClientRect() in conjunction with scroll event listeners. This method returns the size of an element and its position relative to the viewport, allowing developers to calculate manually if an element is visible to the user.

window.addEventListener('scroll', () => {
const element = document.querySelector('.target-element');
const rect = element.getBoundingClientRect();

// Check if the element is in the viewport
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
// Element is visible, do something
}
});

Why it was awful?… Because it runs on the main thread, potentially causing layout thrashing and janky animations. On the other hand, the Intersection Observer is designed to asynchronously observe changes in the intersection of target elements with their parent container or the viewport, with minimal impact on performance. i wanted to show the metric performance but it's so obvious which one is hell :)

Main Thread Load

By offloading observation logic from the main thread, Intersection Observer ensures that user interactions and animations remain smooth, without the stutter or delay that can occur with intensive scroll event handling.
But But …, Although Intersection Observer is efficient, when paired with other event listeners (like scroll or resize events), implementing throttling or debouncing can prevent excessive calculations or DOM manipulations.

At the end, I’ve just put some points of my experience, maybe would be useful. you can call them Best Practice:
Utilizing Root and RootMargin: Guide on effectively using the root and rootMargin properties for custom viewports or margins, accommodating various layout designs and user experiences.

  1. Utilizing Root and RootMargin: Guide on effectively using the root and rootMargin properties for custom viewports or margins, accommodating various layout designs and user experiences.
  2. Choosing Optimal Thresholds: Discuss how to select threshold values that balance between performance and user experience, potentially varying based on the content type or interaction design.
  3. Performance Monitoring: Emphasize the importance of monitoring and testing the performance impact of Intersection Observer in real-world scenarios, using tools like Lighthouse
  4. Tailoring to User Behavior: Consider the typical behavior of your site’s visitors. If they tend to scroll quickly, a larger rootMargin can ensure that content is ready by the time they see it. For slower-scrolling users, a smaller margin might be more efficient.
  5. Responsive Design: Adjust rootMargin based on device or viewport size to account for different browsing behaviors. Mobile users, for example, might benefit from a different margin setting than desktop users due to screen size and interaction patterns.
  6. And don’t forget to take a look at the Reference to get all the details, also THIS helps a lot in deep understanding.

And at the end, as mentioned, I’ve put sample codes in my GitHub.

--

--