🚀AdvancedBrowser Observers
MutationObserver API
Watch for DOM changes added/removed nodes, attribute changes, and text mutations using the MutationObserver API.
Overview
The MutationObserver API watches for changes to the DOM and fires a callback when specified types of mutations occur. It replaces the deprecated DOM Mutation Events (DOMSubtreeModified, DOMNodeInserted, etc.) which caused performance problems by firing synchronously for every change.
MutationObserver fires asynchronously (microtask queue), batching all mutations that occur in a given tick into a single callback invocation. This makes it efficient even when many DOM changes happen at once.
Common use cases: detecting when dynamically added third-party content finishes loading, implementing custom element upgrade logic without Custom Elements v1, tracking attribute changes on elements, auto-resizing textareas, building "DOM watchers" for testing frameworks, and monitoring accessibility changes.
Key Points
- •`new MutationObserver(callback)` callback receives an array of `MutationRecord` objects
- •`observer.observe(target, options)` start watching; options control which mutations to report
- •`childList: true` watch for added/removed child nodes
- •`subtree: true` watch the entire subtree, not just direct children
- •`attributes: true` watch for attribute changes; `attributeFilter: ["class"]` watch specific attributes
- •`characterData: true` watch for text node content changes
- •`observer.disconnect()` stop observing; `observer.takeRecords()` flush pending mutations
Examples
Watching for added nodes (third-party widget injection)
<div id="dynamic-container">
<!-- Third-party script may inject content here -->
</div>
<script>
const container = document.getElementById('dynamic-container');
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
console.log('New element added:', node.tagName);
// Example: find and enhance injected buttons
if (node.matches?.('button')) {
node.classList.add('enhanced');
}
// Find injected elements in subtree
node.querySelectorAll?.('[data-track]').forEach(el => {
setupTracking(el);
});
});
});
});
observer.observe(container, {
childList: true, // watch for added/removed children
subtree: true, // watch the entire subtree
});
</script>Use mutation.addedNodes to react to dynamically injected content from third-party scripts.
Watching attribute changes
<button id="theme-btn" data-theme="light">Toggle Theme</button>
<script>
const btn = document.getElementById('theme-btn');
const attrObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes') {
const el = mutation.target;
const oldVal = mutation.oldValue;
const newVal = el.getAttribute(mutation.attributeName);
console.log(
`${mutation.attributeName} changed: "${oldVal}" → "${newVal}"`
);
// Respond to aria-expanded changes on an accordion
if (mutation.attributeName === 'aria-expanded') {
const isExpanded = newVal === 'true';
el.nextElementSibling.hidden = !isExpanded;
}
}
});
});
attrObserver.observe(btn, {
attributes: true,
attributeFilter: ['data-theme', 'aria-expanded'],
attributeOldValue: true,
});
// Trigger observation
btn.dataset.theme = 'dark'; // fires callback
</script>attributeOldValue: true stores the previous value in mutation.oldValue.
Auto-expanding textarea as user types
<textarea id="auto-expand" rows="3" style="overflow:hidden; resize:none;"></textarea>
<script>
const ta = document.getElementById('auto-expand');
const resizer = new MutationObserver(() => {
ta.style.height = 'auto';
ta.style.height = ta.scrollHeight + 'px';
});
// Observe character data changes (typing)
// We need to observe the text node inside the textarea
// A simpler approach: use 'input' event + MutationObserver together
ta.addEventListener('input', () => {
ta.style.height = 'auto';
ta.style.height = ta.scrollHeight + 'px';
});
// MutationObserver is better for detecting programmatic changes to textarea value
resizer.observe(ta, {
characterData: true,
subtree: true,
childList: true,
});
</script>Common Mistakes
- Modifying the DOM inside a MutationObserver callback and triggering an infinite loop always check if the mutation was caused by your own code before responding
- Not calling `observer.disconnect()` when the component is removed the observer holds a reference to the target and keeps it alive
- Using MutationObserver to replace event listeners for user-triggered changes, events are more appropriate; use MutationObserver for programmatic/third-party DOM changes
Note
In your callback, filter mutations by `mutation.type` and `mutation.target` immediately large subtrees with subtree: true generate many mutations and processing all of them is expensive.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Loading editor…
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4