IntersectionObserver

An API that provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document viewport.

Observers
Widely available (Global 97%+)
Intermediate
Impact: High

When to Use

  • Lazy loading images or heavy components.
  • Implementing infinite scrolling interfaces.
  • Reporting visibility of advertisements.
  • Triggering animations when elements scroll into view.

When NOT to Use

  • Tracking precise pixel-by-pixel scrolling (use passive scroll listeners).
  • Detecting overlaps of elements that do not involve the viewport/scroll container.

Advantages vs Limitations

Advantages

  • Completely asynchronous; it does not run on the main thread during scroll events.
  • Eliminates the need for expensive getBoundingClientRect() calls inside scroll event listeners.
  • Highly customizable using rootMargin to preload content before it enters the viewport.

Limitations

  • Cannot detect if an element is obscured by another visually (e.g. z-index overlap or opacity: 0).
  • Callbacks are fired asynchronously, meaning there is a slight delay (not frame-perfect for synchronized parallax).

Production Examples

Lazy Loading Images Efficiently

This creates a single observer that tracks multiple images. As soon as an image is within 100px of the viewport, its actual source is loaded, and it is unobserved to free memory.

const observer = new IntersectionObserver((entries, obs) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      obs.unobserve(img); // Stop observing once loaded
    }
  });
}, { rootMargin: '100px' });

document.querySelectorAll('img.lazy').forEach(img => observer.observe(img));

Interview Readiness

Q: Why is IntersectionObserver better for performance than listening to the "scroll" event?

A: Listening to the scroll event fires synchronously on the main thread multiple times per frame. To check visibility, developers had to call getBoundingClientRect(), forcing synchronous layout recalculations (thrashing). IntersectionObserver calculates this asynchronously off the main thread.