JavaScript Mutation Observer: Complete Tutorial
Learn how to watch for DOM changes with the MutationObserver API. Detect added nodes, attribute changes, and text updates in real time.
The MutationObserver API watches a DOM element and fires a callback whenever the element's children, attributes, or text content change. Instead of polling the DOM with setInterval or listening to dozens of individual events, you declare what to watch and receive a batch of changes in one efficient, asynchronous callback.
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
console.log(mutation.type, mutation.target);
}
});
observer.observe(document.querySelector("#container"), {
childList: true,
subtree: true,
});The observer now watches the element with id "container" and all its descendants for any child additions or removals. Any time a child element is added or removed anywhere inside that tree, the callback fires with detailed mutation records describing exactly what changed.
How MutationObserver works
A MutationObserver runs asynchronously. When DOM changes happen, the browser queues them and calls your callback once, passing all the changes that occurred since the last callback. This batching is what makes MutationObserver efficient.
Multiple DOM changes that happen in the same synchronous block are collected into one callback invocation. You do not pay the cost of a function call per change.
Observing child list changes
The childList option watches for elements being added to or removed from the target. This is the most common use case, especially for detecting dynamically injected content.
const list = document.querySelector("#list");
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
console.log("Added:", node.textContent);
});
mutation.removedNodes.forEach((node) => {
console.log("Removed:", node.textContent);
});
}
});
observer.observe(list, { childList: true });Each mutation record has an addedNodes NodeList and a removedNodes NodeList. You can iterate them to find exactly which elements appeared or disappeared.
Observing attribute changes
Set attributes to true to detect when attributes like class, id, or data-* change on the target element. Use the attributeFilter array to watch only specific attributes.
const button = document.querySelector("#toggleButton");
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const oldValue = mutation.oldValue;
const newValue = button.getAttribute(mutation.attributeName);
console.log(`${mutation.attributeName}: ${oldValue} -> ${newValue}`);
}
});
observer.observe(button, {
attributes: true,
attributeFilter: ["disabled", "aria-expanded"],
attributeOldValue: true,
});Setting attributeOldValue to true includes the previous value in each mutation record. Without it, you only know that the attribute changed, not what it was before.
Observing text content changes
The characterData option watches for text node changes. Set subtree to true to catch text changes anywhere inside the observed tree.
const editor = document.querySelector("#editor");
const observer = new MutationObserver((mutations) => {
console.log("Text changed in the editor");
});
observer.observe(editor, {
characterData: true,
subtree: true,
});This fires whenever text inside the editor or any of its descendants changes, including contenteditable edits and programmatic textContent assignments.
Disconnecting the observer
Call disconnect to stop observing. This is important when the target element is removed from the DOM or when your component unmounts.
observer.disconnect();After disconnecting, the callback will never fire again. You can call observe again later on the same observer to resume watching a different element.
Mutations that already happened but have not reached the callback yet are still sitting in the observer's internal queue. Call takeRecords right before disconnecting to collect them instead of losing them:
const pendingMutations = observer.takeRecords();
observer.disconnect();
console.log(pendingMutations.length);takeRecords empties the queue and returns whatever mutation records were waiting, so you can process that last batch yourself instead of letting the browser discard it.
Common mistakes
- Forgetting subtree when watching a container with dynamic content. Without it, only direct children trigger the callback.
- Not disconnecting observers when elements are removed. Orphaned observers keep references and prevent garbage collection.
- Using MutationObserver for performance-sensitive animations. It fires asynchronously, which means there is a frame of delay between the DOM change and your callback.
- Observing the entire document body with subtree. This catches every DOM change on the page and can noticeably impact performance.
Related articles
- Tracking DOM Changes with JS Mutation Observers -- practical patterns for reactive DOM watching
- Appending Elements to the DOM in JS: Full Guide -- the kinds of changes MutationObserver detects
- Creating DOM Elements in JavaScript: Complete Guide -- creating elements that trigger observer callbacks
Quick reference
| Operation | Code |
|---|---|
| Create observer | new MutationObserver(callback) |
| Start observing | observer.observe(target, { childList: true }) |
| Watch attributes | { attributes: true, attributeFilter: ["class"] } |
| Watch text changes | { characterData: true, subtree: true } |
| Get old attribute value | { attributeOldValue: true } |
| Stop observing | observer.disconnect() |
| Get pending records | observer.takeRecords() |
Rune AI
Key Insights
- Create a MutationObserver with a callback that receives an array of MutationRecord objects.
- Call observe(target, options) to start watching with childList, attributes, or characterData flags.
- Use subtree: true to watch all descendants, not just direct children.
- The callback receives batched changes, making it far more performant than the old Mutation Events.
- Call disconnect() when done observing to free resources.
Frequently Asked Questions
How is MutationObserver different from Mutation Events?
Does MutationObserver work on elements added dynamically?
Can I observe changes to a specific attribute only?
When should I disconnect a MutationObserver?
Conclusion
MutationObserver gives you a performant way to react to DOM changes without polling. Create an observer with a callback, call observe with the target and options, and your callback fires with a batch of MutationRecord objects whenever the DOM changes in ways you specify.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.