Observers — Intersection, Resize and Mutation
AdvancedObservers 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.
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.
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.
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 AriaHow would you implement infinite scroll without a scroll event listener?
What can ResizeObserver do that the window resize event cannot?
What causes a "ResizeObserver loop" warning?
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.