Home/Learn/JavaScript & TypeScript/Observers — Intersection, Resize and Mutation

Observers — Intersection, Resize and Mutation

Advanced
The Browser

Observers tell you when something changes without polling. IntersectionObserver handles lazy loading and infinite scroll far better than scroll listeners.

Overview

Before observers, answering "is this element visible?" meant a scroll listener calling getBoundingClientRect on every frame — expensive, and it forced layout constantly. The observer APIs invert that: you register interest and the browser tells you, off the main thread where possible. IntersectionObserver is the one you will use most, powering lazy images, infinite scroll and view tracking. ResizeObserver watches element size, which no event ever did. MutationObserver watches the DOM itself, mostly useful when integrating with code you do not control.

IntersectionObserver

Fires when an element enters or leaves the viewport. This is how infinite scroll and lazy loading should be built.

Infinite scroll, without a scroll listener
const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) loadNextPage()
    }
  },
  { rootMargin: '200px' },      // fire 200px early, so it feels instant
)

observer.observe(sentinelElement)     // an empty div after the last row
observer.disconnect()                 // cleanup

// Lazy images need no JavaScript at all:
<img src="thumb.jpg" loading="lazy" />

// The old way, for comparison — runs on every scroll event
// and forces layout each time:
window.addEventListener('scroll', () => {
  if (el.getBoundingClientRect().top < innerHeight) load()
})

ResizeObserver

Element-level resize, which the window resize event cannot give you — an element can change size without the window changing.

Per-element resize, and its loop hazard
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width } = entry.contentRect
    entry.target.classList.toggle('narrow', width < 400)
  }
})
ro.observe(cardElement)

// This is how container queries were done before CSS supported them,
// and it is still how you feed a chart library its dimensions.

// Guard against the loop: writing a style that changes the observed
// size re-triggers the observer. The browser will warn with
// "ResizeObserver loop completed with undelivered notifications".

MutationObserver

Watches DOM changes. Mostly a last resort — needed when a third-party script modifies the page and gives you no hook.

Watching the DOM you do not control
const mo = new MutationObserver((mutations) => {
  for (const m of mutations) {
    if (m.type === 'childList') handleAdded(m.addedNodes)
  }
})

mo.observe(container, {
  childList: true,     // children added or removed
  subtree: true,       // and all descendants
  attributes: true,
  attributeFilter: ['class'],
})

// Fires after the change, in a microtask. If you write to the DOM
// inside the callback you can trigger yourself — disconnect first
// or filter carefully.
mo.disconnect()

Key Points to Remember

  • 1IntersectionObserver replaces scroll listeners for visibility — no per-frame layout reads
  • 2rootMargin lets you trigger before an element is actually visible, which makes infinite scroll feel instant
  • 3Native loading="lazy" on images needs no JavaScript at all
  • 4ResizeObserver watches element size, which the window resize event cannot; beware feedback loops
  • 5MutationObserver is a last resort for reacting to DOM changes made by code you do not control

Interview Questions

Sign in to ask Aria
1

How would you implement infinite scroll without a scroll event listener?

Medium
2

What can ResizeObserver do that the window resize event cannot?

Medium
3

What causes a "ResizeObserver loop" warning?

Hard

Ask Aria about Observers — Intersection, Resize and Mutation

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…