Fossils🌐 Web PlatformBrowser Observer APIs & Implementation
ðŸĢHatchlingBrowserAPIsPerformance

Browser Observer APIs & Implementation

IntersectionObserver, MutationObserver, ResizeObserver — how they work, how to implement them, and why they replaced scroll handlers and polling.

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

ObserverWatchesReplacesCommon Use
IntersectionObserverViewport visibilityScroll handlers + getBoundingClientRectLazy loading, infinite scroll, analytics
MutationObserverDOM changesPolling, deprecated Mutation EventsWidget init, extension detection
ResizeObserverElement sizeWindow resize event + pollingResponsive components, chart resizing
PerformanceObserverPerformance entriesManual 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. null means 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.5 fires 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 rootMargin for 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)