Intersection Observer replaced an entire category of expensive, janky scroll-event-based visibility detection code with a native, asynchronous, purpose-built API โ foundational to many common modern UI patterns.
1The Old, Expensive Approach
Before Intersection Observer existed, detecting whether an element had scrolled into the viewport required listening to the scroll event โ which fires extremely frequently, often dozens of times per second during active scrolling โ and calling element.getBoundingClientRect() inside that handler to manually calculate the element's current position relative to the viewport.
The real problem is that getBoundingClientRect() forces the browser to perform a synchronous layout recalculation at the moment it's called. Doing this repeatedly, especially for multiple tracked elements, inside a handler firing dozens of times per second, causes genuine, measurable performance degradation known as 'layout thrashing' โ directly contributing to janky, stuttering scroll experiences.
2A Purpose-Built, Efficient Solution
new IntersectionObserver(callback, options) creates an observer instance; calling .observe(element) on it registers that element for tracking. The browser then handles visibility detection entirely internally, using efficient, optimized techniques off the critical rendering path, and asynchronously invokes the provided callback only when an observed element's intersection state actually changes โ not continuously during every scroll frame.
Each callback invocation receives an array of IntersectionObserverEntry objects, each with an .isIntersecting boolean and other details (like .intersectionRatio, the precise percentage currently visible), giving far richer information than a manual getBoundingClientRect() calculation would, at a fraction of the performance cost.
3The Foundation Behind Common UI Patterns
Intersection Observer underlies several extremely common modern web patterns. Before the native loading="lazy" attribute (covered in the Modern Images module) existed, it was the standard mechanism for implementing image lazy loading manually โ observing each image and loading its real src only once it neared the viewport.
It remains the standard, efficient approach for infinite scroll pagination (triggering the next page load when a sentinel element near the list's end becomes visible), scroll-triggered fade-in or reveal animations, and tracking advertisement viewability for analytics purposes โ any scenario fundamentally asking 'has this element become visible yet'.
4Step-by-Step Breakdown
Knowing When An Element Becomes Visible, Efficiently. Detecting whether an element has scrolled into view used to mean listening to the scroll event and manually calling getBoundingClientRect() on every single firing โ expensive, janky, and running constantly. Intersection Observer solves this efficiently, natively, asynchronously.
The Old Way Was Expensive: scroll + getBoundingClientRect(). scroll events fire extremely frequently during scrolling, and getBoundingClientRect() forces a synchronous layout recalculation โ calling it inside a scroll handler, especially for many elements, causes real, measurable performance problems known as 'layout thrashing'.
Why The Old Approach Was Costly. Why does calling getBoundingClientRect() repeatedly inside a scroll event handler cause real performance problems?
- โIt doesn't actually cause any real performance cost
- โIt forces a synchronous layout recalculation, expensive when done repeatedly during rapid scroll events
- โIt only causes memory leaks, not performance issues
IntersectionObserver Is Asynchronous And Efficient. new IntersectionObserver(callback) creates an observer that asynchronously notifies your callback only when an observed element's visibility actually changes โ the browser handles the efficient detection internally, off the main thread's critical path, firing far less often than scroll.
IntersectionObserver Efficiency. How does IntersectionObserver avoid the performance cost of the old scroll+getBoundingClientRect() approach?
- โIt still polls just as frequently, but with a smaller function
- โIt asynchronously notifies the callback only when visibility actually changes, handled efficiently by the browser internally
- โIt works by temporarily disabling page scrolling
The Foundational API Behind Lazy Loading And Infinite Scroll. Before native loading="lazy" existed (covered in the Modern Images module), Intersection Observer was the standard way to implement image lazy loading manually โ and it remains the foundation for infinite scroll, scroll-triggered animations, and ad visibility tracking.
Real-World Intersection Observer Use Cases. Before the native loading="lazy" attribute (covered in the Modern Images module) existed, what API was the standard way to implement image lazy loading?
- โResize Observer
- โIntersection Observer
- โMutation Observer
Intersection Observer Introduced. You now understand why the old scroll+getBoundingClientRect() approach was expensive, how IntersectionObserver solves this efficiently and asynchronously, and its role as the foundational API behind lazy loading, infinite scroll, and scroll-triggered animations.
Mark An Element For Observation. IntersectionObserver needs a hook to know which elements to watch as they enter the viewport.
Level Up ๐
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Scroll-Triggered Animations Should Respect prefers-reduced-motion
Intersection-Observer-powered reveal/fade-in animations, while visually appealing, should be disabled or minimized for users who've indicated a preference for reduced motion at the OS level, connecting to broader motion-accessibility best practices.
SEO Implications
- 1
Efficient Visibility Detection Directly Supports Better Scroll Performance And Interaction Metrics
Replacing janky scroll-event-based detection with Intersection Observer can measurably improve real-world responsiveness during scrolling, indirectly supporting metrics like INP from the Core Web Vitals lesson.
Best Practices
Always Prefer Intersection Observer Over Manual scroll + getBoundingClientRect() Detection
It's dramatically more efficient, avoids forced layout recalculation entirely, and is purpose-built exactly for this visibility-detection use case.
Call unobserve() Or disconnect() Once An Element No Longer Needs Tracking
Leaving observers active indefinitely for elements that will never need re-checking (like a lazy-loaded image that's already loaded) wastes resources unnecessarily.
Frequent Bugs
A page with many scroll-triggered animations feels janky and stutters during scrolling.
Replace manual scroll-event-based position checking with Intersection Observer, which avoids the expensive forced layout recalculation causing the jank.
An observer keeps firing for an image long after it has already been lazy-loaded once.
Call observer.unobserve(element) inside the callback once the one-time action (like loading the image) has been completed.
Real-World Examples
A Manual Lazy-Load Implementation
Loading an image's real source only once it nears the viewport, unobserving afterward.
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));