Browser Observer APIs & Implementation
Interview Question: "What browser observer APIs do you know? When and how would you use IntersectionObserver?"
The Senior Answer
"The browser provides four observer APIs â IntersectionObserver, MutationObserver, ResizeObserver, and PerformanceObserver. They all follow the same pattern: create an observer with a callback, observe a target, get notified when something changes. They're architecturally superior to scroll handlers and polling because they're browser-optimized â many run off the main thread and don't cause layout thrashing."
The Four Observers
| Observer | Watches | Replaces | Common Use |
|---|---|---|---|
| IntersectionObserver | Viewport visibility | Scroll handlers + getBoundingClientRect | Lazy loading, infinite scroll, analytics |
| MutationObserver | DOM changes | Polling, deprecated Mutation Events | Widget init, extension detection |
| ResizeObserver | Element size | Window resize event + polling | Responsive components, chart resizing |
| PerformanceObserver | Performance entries | Manual performance.getEntries() | Web Vitals, long task detection |
IntersectionObserver Deep Dive
Why It Exists
// â Old approach â runs 60+ times/sec, forces layout
window.addEventListener('scroll', () => {
const rect = element.getBoundingClientRect(); // Forced layout!
if (rect.top < window.innerHeight) {
loadImage(element);
}
});
// â
IntersectionObserver â browser-optimized, no layout thrashing
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) loadImage(entry.target);
});
});
observer.observe(element);Implementation: Lazy Loading
function createLazyLoader() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
observer.unobserve(img);
});
},
{ rootMargin: '200px' }
);
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
return () => observer.disconnect();
}Implementation: Infinite Scroll
function setupInfiniteScroll(sentinelEl, loadMore) {
let loading = false;
const observer = new IntersectionObserver(
async ([entry]) => {
if (!entry.isIntersecting || loading) return;
loading = true;
await loadMore();
loading = false;
},
{ rootMargin: '400px' }
);
observer.observe(sentinelEl);
return () => observer.disconnect();
}Implementation: Scroll-Triggered Animations
function animateOnScroll(selector, animationClass) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add(animationClass);
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.2 }
);
document.querySelectorAll(selector).forEach(el => observer.observe(el));
}React Hook
function useInView(options = {}) {
const [isInView, setIsInView] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(
([entry]) => setIsInView(entry.isIntersecting),
options
);
observer.observe(ref.current);
return () => observer.disconnect();
}, [options.threshold, options.rootMargin]);
return [ref, isInView];
}
// Usage
function ProductCard({ product }) {
const [ref, isVisible] = useInView({ rootMargin: '100px' });
return (
<div ref={ref}>
{isVisible ? <ProductImage src={product.image} /> : <Placeholder />}
</div>
);
}Follow-Up Questions
"What are the IntersectionObserver options?"
"Three options:
rootâ The scrollable ancestor to observe against.nullmeans viewport.rootMarginâ Margin around the root, like CSS margin.'200px'triggers 200px before the element enters the viewport â great for prefetching.thresholdâ A number or array of ratios (0 to 1) at which the callback fires.0.5fires when half the element is visible.[0, 0.5, 1]fires at 0%, 50%, and 100% visibility."
"How would you implement a 'read percentage' tracker?"
function trackReadProgress(articleEl, onProgress) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
onProgress(entry.intersectionRatio);
});
},
{ threshold: Array.from({ length: 20 }, (_, i) => i / 20) }
);
observer.observe(articleEl);
}"When would you use MutationObserver?"
"When you need to react to DOM changes you don't control â third-party scripts injecting elements, CMS content loading, browser extensions modifying your page. It's also useful for auto-initializing components added to the DOM dynamically."
"When would you use ResizeObserver?"
"When component behavior depends on its own size, not the window size. CSS container queries cover some cases, but ResizeObserver is needed for JavaScript-driven behavior â resizing charts, switching between compact/full layouts, or notifying a parent about size changes."
"What about cleanup?"
"Always call
observer.disconnect()in cleanup â useEffect return, component unmount, or page teardown. Observers hold references to observed elements, so not disconnecting can leak memory."
The One-Liner
"Observer APIs are the browser telling you: 'Stop polling. Stop listening to scroll events. Tell me what you care about, and I'll tell you when it happens â efficiently.'"
Red Flags
- Using scroll handlers for lazy loading (IntersectionObserver is the modern answer)
- Not knowing
rootMarginfor pre-loading before visibility - Forgetting cleanup /
disconnect() - Only knowing IntersectionObserver but not MutationObserver or ResizeObserver
- Not explaining WHY observers are better than scroll handlers (off-main-thread, no forced layout)