MutationObserver watches for changes to the DOM tree — added/removed nodes, attribute changes, text content changes — efficiently and asynchronously, without the severe performance cost of the deprecated Mutation Events it replaced.
1The Mutation Observer API | JavaScript Tutorial - In-Depth Guide Part 1
MutationObserver watches a DOM node (and optionally its descendants) for changes, batching them into an array of mutation records delivered asynchronously to your callback.
const observer = new MutationObserver((mutations) => {
mutations.forEach((m) => console.log(m.type, m.target));
});
observer.observe(document.body, { childList: true, subtree: true });Watching DOM Changes
2The Mutation Observer API | JavaScript Tutorial - In-Depth Guide Part 2
The observe() options object controls exactly what kinds of changes are tracked — childList for added/removed children, attributes for attribute changes, and characterData for text content changes.
observer.observe(target, {
childList: true, // added/removed children
attributes: true, // attribute value changes
characterData: true, // text node content changes
subtree: true, // include all descendants, not just direct children
});Configuring What to Observe
3The Mutation Observer API | JavaScript Tutorial - In-Depth Guide Part 3
A common use case is reacting to DOM changes made by third-party scripts or browser extensions that you don't control and can't hook into directly.
const observer = new MutationObserver((mutations) => {
mutations.forEach((m) => {
m.addedNodes.forEach((node) => {
if (node.classList?.contains('third-party-banner')) applyCustomStyles(node);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });Reacting to Third-Party Changes
4The Mutation Observer API | JavaScript Tutorial - In-Depth Guide Part 4
MutationObserver replaced the older, deprecated Mutation Events (like DOMNodeInserted), which fired synchronously for every single change and could severely degrade performance on active pages.
// Deprecated, avoid:
element.addEventListener('DOMNodeInserted', handler);
// Modern replacement:
new MutationObserver(handler).observe(element, { childList: true });Replacing Mutation Events
5The Mutation Observer API | JavaScript Tutorial - In-Depth Guide Part 5
Always call disconnect() when you no longer need to watch for changes, and takeRecords() can retrieve any pending, not-yet-delivered mutation records before disconnecting.
const pending = observer.takeRecords(); // get any undelivered records
processMutations(pending);
observer.disconnect();Cleanup and takeRecords()
6Step-by-Step Breakdown
MutationObserver watches a DOM node (and optionally its descendants) for changes, batching them into an array of mutation records delivered asynchronously to your callback.
Checkpoint: Are mutation records delivered synchronously, immediately as each individual change happens?
- →Yes, exactly like the old Mutation Events
- →No, they are batched and delivered asynchronously
The observe() options object controls exactly what kinds of changes are tracked — childList for added/removed children, attributes for attribute changes, and characterData for text content changes.
A common use case is reacting to DOM changes made by third-party scripts or browser extensions that you don't control and can't hook into directly.
MutationObserver replaced the older, deprecated Mutation Events (like DOMNodeInserted), which fired synchronously for every single change and could severely degrade performance on active pages.
Checkpoint: Why was MutationObserver introduced to replace Mutation Events?
- →Mutation Events' synchronous firing caused severe performance problems
- →Purely for a shorter, more convenient syntax
Always call disconnect() when you no longer need to watch for changes, and takeRecords() can retrieve any pending, not-yet-delivered mutation records before disconnecting.
Next, we'll explore 'Introduction to Web Workers'.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Use MutationObserver to Patch Accessibility Gaps in Third-Party Widgets
When a third-party embed injects DOM elements missing proper ARIA attributes, a MutationObserver can detect those insertions and programmatically add the missing accessibility attributes, as a last-resort patch when you can't modify the third-party script directly.
SEO Implications
- 1
No Direct SEO Effect
MutationObserver is a client-side DOM-monitoring tool; SEO relevance is limited to ensuring any reactive DOM patches it drives do not introduce rendering inconsistencies.
Best Practices
Enable Only the Specific Observe Options You Actually Need
Watching for every kind of mutation (attributes, childList, characterData, subtree) on a large DOM tree generates unnecessary overhead; scope the configuration to exactly what your callback needs to react to.
Always Disconnect Observers That Are No Longer Needed
An observer left running on a removed or irrelevant part of the DOM continues consuming resources and firing callbacks for changes nobody is acting on.
Frequent Bugs
Observing with `subtree: true` and `childList: true` on a very large, frequently-changing container (like a chat log), causing excessive mutation records and a sluggish callback that struggles to keep up.
Scope the observed target more narrowly if possible, or debounce/batch processing of the mutation records inside the callback rather than reacting to every single one individually.
Forgetting to disconnect a MutationObserver when the observed element is removed from the DOM, leaving it silently inactive but still referenced in memory.
Explicitly call disconnect() as part of the same cleanup logic that removes or unmounts the observed element/component.
Real-World Examples
Detecting and Styling Dynamically Injected Third-Party Content
A page embedded a third-party chat widget that injected its own DOM elements after the page loaded, and needed custom styling applied to match the site's design once those elements appeared.
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
if (node.id === 'chat-widget-root') applyBrandStyles(node);
});
}
});
observer.observe(document.body, { childList: true, subtree: true });