ReviseAlgo Logo

DOM Manipulation

Scroll Events & Intersection Observer

Master element visibility observation in JavaScript. Contrast scroll listeners with the Intersection Observer API for performance optimizations.

Last Updated: July 15, 2026 10 min read

1. Introduction

In order to load assets lazily or build infinite scroll lists, you must be able to track when an element is visible on the screen. Historically, this required listening to scroll events and calculating element positions manually. Modern browsers provide the Intersection Observer API to handle element visibility changes efficiently.

2. Why It Matters

Scroll events trigger continuously as the page is scrolled, running on the main execution thread. Reading geometric properties (like getBoundingClientRect()) inside scroll listeners forces the browser to run synchronous layout calculations, which causes layout thrashing and slows down page performance. The Intersection Observer API runs asynchronously, avoiding these performance bottlenecks.

3. Real-World Analogy

Think of a Warehouse Guard monitoring shipping gates:

  • Scroll Listeners (Constant Inspections): The guard runs down the hallways every 10 milliseconds to check if a shipping container has arrived at a gate. This constant running quickly exhausts the guard (blocks the main thread), even if no container arrives for hours.
  • Intersection Observer (Automatic Gate Alarm): The guard installs an automated light sensor at the gate threshold. The guard sits at the desk reading reports. The moment a container breaks the light beam (enters the viewport), an alarm rings (callback fires) to notify the guard. The guard only reacts when the event occurs, saving energy.

4. The Intersection Observer API

The Intersection Observer API registers a callback to run whenever a target element intersects (crosses) another element or the browser viewport:

5. Practical Example

This script demonstrates implementing lazy loading for images, swapping the placeholder src attribute only when the image enters the viewport:

6. Common Mistakes

  • Running heavy layout calculations inside scroll listeners: Reading properties like element.offsetTop inside scroll listeners causes layout thrashing. If you must use scroll listeners, always wrap the callback in a throttle or debounce utility, or use requestAnimationFrame to batch the calculations.
  • Forgetting to call unobserve: Forgetting to call unobserve() when an element is no longer needed (like a lazy loaded image that has already finished loading) keeps the observer in memory, wasting resources.

7. Quick Quiz

Q1: Which API is recommended for implementing smooth infinite scroll lists in modern web browsers?

A) Scroll event listeners querying getBoundingClientRect()

B) Intersection Observer API

Answer: B — The Intersection Observer API handles visibility changes asynchronously, avoiding the performance bottlenecks of scroll listeners.

8. Scenario-Based Challenge

The Sticky Header Transition:

You want to add a class: .is-sticky to a header element when the page is scrolled past the hero section. Design an optimized visibility check using an Intersection Observer that monitors a tiny sentinel div at the top of the page.

9. Debugging Exercise

Explain why this infinite scroll trigger fires repeatedly and crashes the API:

const sentinel = document.getElementById('sentinel');
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Objective: Fetch next page of data
      loadNextPage(); // appends items, but sentinel remains at the bottom viewport!
    }
  });
});
observer.observe(sentinel);
View Solution

Diagnosis: The sentinel element remains visible in the viewport while the API request is loading. Since the sentinel remains visible, the observer continues to trigger the callback repeatedly, firing duplicate API requests.

Fix: Temporarily stop observing the sentinel when loading, and resume observation only after the new items have finished rendering and the sentinel has been pushed down below the viewport:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(async (entry) => {
    if (entry.isIntersecting) {
      observer.unobserve(sentinel); // Stop observing during load
      await loadNextPage(); // Wait for data to load and render
      observer.observe(sentinel); // Resume observation
    }
  });
});

10. Interview Questions

🟢 Q1: Explain why the Intersection Observer API is more performant than scroll event listeners.

Answer:
Scroll Listeners: Trigger continuously on the main execution thread as the user scrolls, which can cause frame drops. Reading geometric properties (like getBoundingClientRect()) inside scroll callbacks forces the browser to run synchronous layouts, causing layout thrashing.
Intersection Observer: Runs asynchronously. The browser handles visibility calculations in the background, only queueing a microtask to execute the callback when the target element crosses the specified threshold, which avoids blocking the main thread.

11. Production Considerations

  • Cleanup Observers: When building components in frameworks like React, always call observer.disconnect() inside the component teardown lifecycle (like the useEffect cleanup return) to remove all target element observations and prevent memory leaks.