DEV Community

Cover image for The Secret Life of JavaScript: The Observer
Aaron Rose
Aaron Rose

Posted on

The Secret Life of JavaScript: The Observer

Stop Polling the DOM: Mastering the Intersection Observer API


Timothy leaned back in his chair, listening to the sudden, aggressive whir of his laptop fan. He had just finished implementing a lazy-loading feature for a massive grid of user profile pictures.

"The scroll is perfectly smooth," Timothy said, tapping his screen. "I used the { passive: true } flag we talked about yesterday. The Compositor Thread is completely unblocked. But my CPU usage just spiked to ninety percent, and my laptop sounds like it is preparing for takeoff."

Margaret strolled over, her dark roast coffee in hand, and peered at the performance monitor on his secondary display.

"You successfully unblocked the train," Margaret said, nodding at the screen. "But you are torturing the dispatcher."

She pointed to the block of code responsible for the lazy loading.

const images = document.querySelectorAll('img[data-src]');

window.addEventListener('scroll', () => {
  images.forEach(img => {
    // Calculate exact geometry on every scroll tick
    const rect = img.getBoundingClientRect();

    // If the image enters the viewport, load it
    if (rect.top < window.innerHeight) {
      img.src = img.dataset.src;
    }
  });
}, { passive: true });

Enter fullscreen mode Exit fullscreen mode

"You remembered our lesson on Layout Thrashing," Margaret explained. "Every time you call getBoundingClientRect(), you force the browser to calculate the exact, pixel-perfect geometry of the DOM. Doing that is expensive."

Timothy defended his code. "But I have to know where the images are so I can load them before the user sees blank spaces."

"Yes, but look at when you are asking," Margaret said. "You tied that heavy mathematical calculation to the scroll event. Even though it is a passive listener, that event fires dozens of times a second. You are forcing the Main Thread to frantically calculate the exact GPS coordinates of every single passenger, every time the train moves an inch. You are essentially sitting in the backseat of a car, poking the driver fifty times a second, screaming, 'Are we there yet? Are we there yet?'"

"So how do I know when the image enters the screen without asking?" Timothy asked.

"You stop polling, and you set a tripwire," Margaret smiled. She introduced a new concept to the whiteboard. "You use the IntersectionObserver API."

"An Observer flips the entire architecture," Margaret continued. "Instead of using JavaScript to constantly ask the browser for geometric coordinates, you hand a list of elements over to the browser's highly optimized, internal C++ engine. The browser engine natively understands the viewport. It handles all the spatial mathematics quietly in the background. The Main Thread goes completely to sleep, and the browser simply taps your JavaScript on the shoulder exactly when an element crosses the threshold."

"And you can configure exactly where that tripwire sits," she added. "By setting a rootMargin of, say, 100px, you tell the browser to tap your shoulder just before the image enters the screen, completely eliminating that split-second white flash. You can even use the threshold option to wait until exactly 50% or 100% of the element is visible."

Timothy deleted his scroll listener and his getBoundingClientRect() loop entirely. He created an options object and instantiated a new IntersectionObserver.

const images = document.querySelectorAll('img[data-src]');

const options = {
  rootMargin: '100px', // The tripwire: fire 100px before entering the viewport
  threshold: 0         // Fire as soon as 1 pixel crosses the line
};

// 1. Create the tripwire
const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    // 2. The browser taps JavaScript on the shoulder
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // Load the image

      // 3. Stop watching this specific image once it loads
      observer.unobserve(img); 
    }
  });
}, options);

// 4. Register all images with the internal C++ engine
images.forEach(img => observer.observe(img));

Enter fullscreen mode Exit fullscreen mode

"Look at the elegance of that," Margaret said, watching him save the file. "There is zero math on the Main Thread. Modern frameworks like React actually wrap this entire C++ engine interaction into simple hooks like useInView(), giving you this performance benefit with even less code. But underneath, the architecture is exactly what you just wrote: The JavaScript only wakes up when an image actually triggers the tripwire, it does exactly one job, and then it immediately unobserves the element so it never fires again."

Timothy refreshed the page and began scrolling furiously down the massive grid of profiles.

The scrolling remained buttery smooth. The images popped into existence perfectly just before they crossed into view. The dispatcher was no longer being poked constantly; he was waiting patiently, and the browser itself was sending a signal only when a passenger actually needed to get off.

Best of all, the frantic whirring of his laptop fan slowly spun down into complete silence.


Aaron Rose is a software engineer and technology writer at tech-reader.blog. For explainer videos and podcasts, check out Tech-Reader YouTube channel.

Top comments (8)

Collapse
 
pengeszikra profile image
Peter Vivo

I like your storytelling.

Collapse
 
aaron_rose_0787cc8b4775a0 profile image
Aaron Rose

🙏✨❤

Collapse
 
trinhcuong-ast profile image
Kai Alder

The "tripwire" analogy is perfect for explaining how IntersectionObserver works. Took me embarrassingly long to stop using scroll listeners for lazy loading in my own projects.

One thing worth mentioning — if anyone's using this for infinite scroll, don't forget to unobserve the sentinel element after triggering the fetch and then re-observe a new one. I've seen memory leaks in production from people observing hundreds of sentinel divs that never get cleaned up.

Also curious if you're planning to cover MutationObserver and ResizeObserver in this series? ResizeObserver in particular is one of those APIs that feels like magic once you stop using resize event listeners on window.

Collapse
 
aaron_rose_0787cc8b4775a0 profile image
Aaron Rose

Kai,

That unobserve tip is production gold — the kind of thing that never shows up in tutorials but absolutely shows up in memory profilers at 2am. Thank you for adding it here where others will find it. 💯

And yes — MutationObserver and ResizeObserver are both on the roadmap.

You've described ResizeObserver exactly right. The moment you stop wrestling with window resize events and debounce timers and just... observe the element itself, something clicks. It feels like the API that should have existed all along.

Margaret might say these three Observers together — Intersection, Mutation, Resize — are the browser finally learning to watch the world the way developers always needed it to. Each one a different kind of attention. 🌹💯❤

Collapse
 
marina_eremina profile image
Marina Eremina

I was always forgetting what the rootMargin property does exactly in the IntersectionObserver, now I think I'll finally be able to memorize it 🙂 Such analogies work really well!

Collapse
 
aaron_rose_0787cc8b4775a0 profile image
Aaron Rose

Marina,

I'm glad that analogy worked for you! That's exactly why I like analogies — they really help lock concepts into memory. Once you see rootMargin as the invisible fence around the tripwire, it stops being an abstract number and starts making spatial sense.

Cheers!🌹

Collapse
 
tanelith profile image
Emir Taner

Thank you for sharing!

Collapse
 
aaron_rose_0787cc8b4775a0 profile image
Aaron Rose

❤🙏✨