JavaScript Observer APIs Table

ObserverWatchesField note
IntersectionObserverEnters/leaves the viewportLazy-load and infinite scroll - replaces scroll-math forever
{ threshold: 0.5 }Fire at visibility fractionArray of thresholds - 0 = any pixel, 1 = fully visible
{ rootMargin }Grow the watched box200px preload margin - images ready BEFORE they scroll in
MutationObserverDOM changesAttribute/childList/characterData - react to markup you do not own
ResizeObserverElement box changesPer-ELEMENT (not window) - container-driven widgets without media queries
PerformanceObserverMetrics streamLCP, longtasks - the Core Web Vitals collection pipe
entry.isIntersectingThe lazy-load gateUnobserve after first fire - one-shot loading
disconnect()Stop watchingSame teardown discipline as event listeners
Reference: the MDN Intersection Observer reference. The observer family replaced scroll-event math with subscriptions: IntersectionObserver answers "is it visible yet" without reading layout (lazy-load, infinite scroll, view tracking), MutationObserver reacts to DOM changes in markup you do not own, ResizeObserver watches ELEMENT boxes - not the window - which is how widgets respond to their container rather than the viewport. Bottom line: rootMargin extends the intersection box so lazy images load BEFORE entering the viewport (200px kills the pop-in), unobserve after the first intersection for one-shot loads, and disconnect belongs in every teardown path. Related tools: event listeners table (the scroll listeners observers replace), event loop table (when observer callbacks run), and img table (native loading=lazy vs observer-built lazy).

The observer family replaced scroll-event math with subscriptions: IntersectionObserver answers 'is it visible yet' without reading layout, MutationObserver reacts to DOM changes, ResizeObserver watches element boxes (not the window), and PerformanceObserver streams metrics. Ask once, get called back.

Bottom line: rootMargin extends the intersection box - a 200px bottom margin fires lazy loads BEFORE images scroll into view, which is what kills the pop-in effect native-feeling sites need. One-shot loads unobserve after the first intersection; every teardown path calls disconnect.

The honest part: observer callbacks are asynchronous and batched - they run after layout, at a time the browser chooses, so you cannot read them synchronously inside the same tick. That asynchrony is the feature (no layout thrash), but it means an intersection handler must not assume the exact frame.

How to use

  1. Lazy-load properly: new IntersectionObserver(entries => { if (entries[0].isIntersecting) { load(); obs.unobserve(el); } }, { rootMargin: '200px' }) - preload before arrival, fire once.
  2. Infinite scroll without scroll events: observe a SENTINEL div after the last item - when it intersects, fetch the next page and re-append.
  3. Watch your container: new ResizeObserver(entries => relayout(entries[0].contentRect)) on a widget root - responsive to the container, no media query coupling.

Frequently asked questions

How is IntersectionObserver different from a scroll listener plus getBoundingClientRect?

Inversion of cost. The scroll approach runs your math on every scroll event, reads layout synchronously (getBoundingClientRect can force reflow), and requires you to debounce or rAF-throttle to avoid jank. IntersectionObserver inverts it: you declare WHICH element and WHAT visibility threshold, the browser calls you back asynchronously after layout when the condition changes - no per-event work, no layout reads, no thrash. The old pattern is also wrong at the edges: scroll listeners miss programmatic scrolls (anchor jumps) in some setups, while intersection is driven by actual visibility, whatever caused it.

What do threshold and rootMargin actually control?

Threshold sets the visibility FRACTION that triggers: 0 fires when any pixel appears (default), 0.5 at half-visible, 1 when fully visible - and it accepts an array, calling back at each stage (a view-tracking widget might want 0.25, 0.5, 0.75). rootMargin grows or shrinks the ROOT box before intersecting: '200px 0px' effectively treats the viewport as extending 200px further down, so lazy images start loading before they enter - the single most important knob for killing pop-in. Negative margins shrink it, delaying triggers for expensive content.

When do I need MutationObserver instead of just hooks in my own code?

When the DOM changes come from code you do not own: third-party scripts, browser extensions, content editable, or framework internals. MutationObserver subscribes to childList (added/removed nodes), attributes and characterData changes - the building block for ad blockers, translation overlays, rich-text editors reacting to user edits, and test harnesses. For your own components, direct function calls are faster and clearer; the observer earns its keep precisely at the boundary where someone else's code mutates the page and you need to react without patching their source.

Why does ResizeObserver matter when window.resize exists?

Different questions. window.resize answers 'how big is the SCREEN' - useless for a component inside a flex sibling, a resizable panel, or a container-query-style layout, where the viewport never changed but the element's box did. ResizeObserver watches a specific ELEMENT's content box and fires when IT changes for any reason (window resize, sibling content growth, font load, container drag). That is the primitive container-adaptive widgets were built on before CSS container queries: a chart that redraws to its card's width, independent of how the card got its size. The loop caution: resizing INSIDE your own ResizeObserver callback can re-trigger it - guard or debounce your relayouts.

Related tools