MutationObserver

MutationObserver is a browser API that watches a part of the DOM and calls you back with a list of exactly what changed — nodes added or removed, attributes edited, or text rewritten.

Learn MutationObserver in our free JavaScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

It is how you react to DOM you do not control: a widget injecting elements, content loaded by another script, or attributes flipping as a page updates itself.

💡 About the runnable demos: The real MutationObserver needs a live DOM, so it stays in the read-only code blocks. Each Try it Yourself runs a plain-JS diff that reports what changed between two snapshots of a tree — the same "here is the list of changes" shape that real mutation records give you.

📹 Real-World Analogy: A MutationObserver is a security camera pointed at one room. You do not stare at the feed all day; the system hands you a highlight reel of everything that moved while you were away — "a box was added here, a label was changed there". You choose what the camera watches (nodes, attributes, text) and review the clips in a batch.

Create an observer with a callback, then call observe(target, options) to choose what to watch. The callback receives an array of MutationRecord objects, each describing one change: its type ( "childList" , "attributes" , or "characterData" ), the affected node, and lists of addedNodes / removedNodes .

At its heart, this is a diff : compare "before" and "after" and report what was added or removed. Here is that diff running for real:

Turn on attributes: true to be told when an attribute changes; add attributeOldValue: true and each record carries the previous value in oldValue . Turn on characterData: true to catch edits to text node contents. You can also restrict attribute watching to specific names with attributeFilter .

The same "what changed" diff, now over an object's attributes:

Replace the blank with the option that makes the observer watch added/removed children.

Records are batched and delivered as a microtask after the current script runs, so several quick DOM edits arrive together. Call disconnect() to stop observing entirely. Call takeRecords() to pull out any queued-but-not-yet-delivered records immediately — handy right before disconnecting so nothing is lost.

⚠️ Avoid infinite loops: If your callback edits the same DOM it observes, it can trigger itself forever. Either disconnect() before editing and re-observe after, or guard against the change you just made.

This model batches changes into a queue, then lets you drain it and stop — exactly like the real API:

Replace the blank with the method that stops the observer.

childList: true . Add subtree: true to watch all descendants too.

Pass attributeOldValue: true ; the record then has oldValue .

takeRecords() keeps observing (it just drains the queue); disconnect() stops entirely.

Combine both ideas: write a diffTree that reports added children, removed children, AND changed attributes between two snapshots — a tiny version of what MutationObserver hands you.

Up next: IndexedDB — a real database in the browser. 🗄️

Practice quiz

What does a MutationObserver watch?

  • Element visibility on scroll
  • Network requests
  • Changes to the DOM such as nodes added/removed, attributes, and text
  • CPU usage

Answer: Changes to the DOM such as nodes added/removed, attributes, and text. A MutationObserver watches a part of the DOM and reports what changed — nodes added/removed, attributes edited, or text rewritten.

Which option watches nodes being added or removed?

  • childList: true
  • attributes: true
  • characterData: true
  • subtree: true

Answer: childList: true. childList: true watches direct children being added or removed.

What does adding subtree: true do?

  • Watches only the target node
  • Disables the observer
  • Watches network changes
  • Watches the entire descendant tree of the target, not just direct children

Answer: Watches the entire descendant tree of the target, not just direct children. subtree: true extends watching to the whole descendant tree of the target.

How do you capture an attribute's previous value?

  • Read attribute.previous
  • Pass attributeOldValue: true so each record has oldValue
  • Use characterData: true
  • It is automatic

Answer: Pass attributeOldValue: true so each record has oldValue. attributeOldValue: true makes each record carry the previous value in oldValue.

What does the callback receive?

  • An array of MutationRecord objects
  • A single change object
  • The target element
  • A boolean

Answer: An array of MutationRecord objects. The callback receives an array of MutationRecord objects, each describing one change.

Why are mutation records delivered in a batch?

  • To encrypt them
  • To slow down the page
  • For performance — all mutations in a tick are delivered together via a microtask
  • Because the DOM requires it

Answer: For performance — all mutations in a tick are delivered together via a microtask. The browser batches all mutations in a tick and delivers them together as a microtask, avoiding firing the callback hundreds of times.

What does disconnect() do?

  • Drains the pending queue but keeps observing
  • Stops the observer entirely until you call observe() again
  • Deletes the target node
  • Adds a new option

Answer: Stops the observer entirely until you call observe() again. disconnect() stops observation completely; you must call observe() again to resume.

What does takeRecords() do?

  • Stops observing
  • Creates a new observer
  • Deletes records permanently from the DOM
  • Empties and returns queued-but-not-yet-delivered records without stopping observation

Answer: Empties and returns queued-but-not-yet-delivered records without stopping observation. takeRecords() drains any pending records immediately while observation continues — often called right before disconnect().

What risk exists if your callback edits the same DOM it observes?

  • Nothing
  • It can trigger itself in an infinite loop
  • The page reloads
  • It changes the threshold

Answer: It can trigger itself in an infinite loop. Editing the observed DOM inside the callback can re-trigger the observer forever; disconnect first or guard the change.

What is a real-world use of MutationObserver?

  • Lazy-loading images on scroll
  • Storing data offline
  • Detecting when a third-party widget or ad injects elements into the page
  • Making network requests

Answer: Detecting when a third-party widget or ad injects elements into the page. It is ideal for reacting to DOM you do not control, such as detecting injected widgets, ads, or content from other scripts.