Under The Hood
frontend javascript DOM spec (MutationObserver)

MutationObserver: Batched Microtask DOM Change Delivery

Last updated
Prerequisites:
DOM manipulation basics
Understanding of the event loop and microtasks
  • mutation-observer
  • dom
  • microtask
  • batching
  • observers
  • performance

Read at your depth

The practical view

MutationObserver watches DOM changes and fires a callback with a list of all mutations that happened since the last delivery. const obs = new MutationObserver((records) => { /* records is an array */ }); obs.observe(target, { childList: true, subtree: true, attributes: true });. The callback is delivered asynchronously, batched: if you make 100 DOM changes in one synchronous block, the observer fires ONCE with 100 records. This makes it efficient for reacting to content changes (e.g. re-measuring, highlighting, or syncing state). Use disconnect() to stop. Unlike mutation events (deprecated), it doesn't fire for every single change synchronously and doesn't break the page.

The same idea in other frameworks

react equivalent
React uses its own renderer; MutationObserver is used by testing/analysis tooling (e.g. detecting re-render counts) rather than core rendering.

Legacy vs modern

DOM mutation events (deprecated) vs MutationObserver batching

Old mutation events fired synchronously per change and broke the page; MutationObserver batches records and delivers them in a microtask.

before → after
DOMNodeInserted / mutation events
document.addEventListener('DOMNodeInserted', handler); // ❌ OLD: fires synchronously for EVERY insertion
MutationObserver batching
const obs = new MutationObserver(records => handle(records)); // ✅ NEW: batched, async delivery
obs.observe(root, { childList: true, subtree: true });

Interview gotchas

Context

Interviewers want the design rationale — batching plus microtask delivery — because it explains the API's performance profile and why old mutation events were removed.

The mechanical answer

Synchronous per-change delivery (the old mutation events) caused two problems: it interrupted the middle of the mutating code, forcing layout/rendering work inline and re-entrantly, and a handler that mutated the DOM would re-trigger the event, causing infinite loops. MutationObserver decouples notification from mutation: changes are queued into records, and a microtask is scheduled. At the microtask checkpoint after the current task, the callback runs once with the full record list. This makes observing your own writes safe (no synchronous feedback loop) and keeps delivery off the critical rendering path. The cost is that records are allocated per change, so observers should be disconnected when unused.

Trap

The naive answer is 'MutationObserver fires after every DOM change'. It fires once per microtask checkpoint with ALL accumulated changes, so the callback may run less often than you mutate — relying on one-callback-per-change breaks code. Another trap: assuming the callback runs synchronously after each mutation; it runs at the end of the task, so you cannot read the immediately-post-mutation DOM state inside the same synchronous block expecting the observer to have run.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.