The MutationObserver API watches for changes to the DOM — element additions, removals, attribute changes, and text content changes. It's the modern replacement for DOM Mutation Events (deprecated). Firefox has full support, and it's essential for building reactive components, debugging DOM changes, and writing Firefox extensions that respond to page modifications.
What MutationObserver Watches
MutationObserver can detect four types of DOM changes:
- Child list changes — elements added or removed
- Attribute changes — attributes added, removed, or changed
- Character data changes — text content of text nodes changed
- Subtree changes — any of the above within descendant elements
Basic Setup
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Mutation type:', mutation.type);
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
console.log('Added:', node);
});
mutation.removedNodes.forEach(node => {
console.log('Removed:', node);
});
}
if (mutation.type === 'attributes') {
console.log('Attribute changed:', mutation.attributeName);
console.log('Old value:', mutation.oldValue);
console.log('New value:', mutation.target.getAttribute(mutation.attributeName));
}
});
});
// Start observing
observer.observe(document.getElementById('container'), {
childList: true, // Watch for child element add/remove
attributes: true, // Watch for attribute changes
characterData: true, // Watch for text content changes
subtree: true, // Watch descendants, not just direct children
attributeOldValue: true, // Include old attribute value in mutation record
characterDataOldValue: true // Include old text content in mutation record
// attributeFilter: ['class', 'data-state'] // Watch only specific attributes
});
// Stop observing
observer.disconnect();
Use Case: Detecting Dynamic Content Injection
Single-page apps and frameworks add/remove DOM elements asynchronously. MutationObserver detects these changes:
// Watch for new elements matching a selector
function waitForElement(selector) {
return new Promise(resolve => {
const existing = document.querySelector(selector);
if (existing) { resolve(existing); return; }
const observer = new MutationObserver((mutations) => {
const target = document.querySelector(selector);
if (target) {
observer.disconnect();
resolve(target);
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// Usage in extension or script
const dynamicButton = await waitForElement('.dynamic-submit-button');
dynamicButton.click();
This pattern is heavily used in browser extensions that need to interact with SPA content that loads after the initial page.
Use Case: Watching Class Changes
const button = document.getElementById('myButton');
const classObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'class') {
const newClass = button.className;
if (button.classList.contains('active')) {
handleActivated();
} else {
handleDeactivated();
}
}
});
});
classObserver.observe(button, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true
});
Use Case: Auto-Resize Textarea
// Watch for text changes to auto-resize
function autoResize(textarea) {
const observer = new MutationObserver(() => {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
});
// MutationObserver doesn't watch input events, but this pattern works with input events:
textarea.addEventListener('input', () => {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
});
}
// Note: for input element value changes, use 'input' events, not MutationObserver
// MutationObserver watches DOM, not form control values
Firefox Extension: Detecting Page Changes
Extensions use MutationObserver in content scripts to respond to SPA navigation (which doesn't fire traditional page load events):
// content_script.js
let lastUrl = location.href;
const navigationObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
onPageChanged(lastUrl);
}
});
navigationObserver.observe(document.body, { childList: true, subtree: true });
function onPageChanged(url) {
// Re-inject extension UI, update state, etc.
injectExtensionUI();
}
Performance Considerations
MutationObserver callbacks are batched — all mutations that happen before the next microtask checkpoint are collected into one callback invocation. This prevents the callback from firing thousands of times per second.
However, watching the entire document with subtree: true on a mutation-heavy SPA can still create performance overhead. Best practices:
-
Scope your observations: watch the smallest subtree that matters, not
document.bodyfor everything -
Disconnect when done:
observer.disconnect()immediately when you've found what you're looking for -
Use
attributeFilter: list specific attributes rather than watching all attributes
Debugging DOM Changes with DevTools
Firefox DevTools has a built-in DOM change detector:
Method 1: Inspector → right-click any element → "Break on..." → "Subtree Modifications"
This pauses JavaScript execution at the point where the DOM modification happens, with a full call stack. Better than MutationObserver for debugging because you see exactly what code caused the change.
Method 2: Use MutationObserver in the DevTools console to find unknown DOM mutators:
// In DevTools console — find what's modifying an element
const target = document.getElementById('mystery-element');
new MutationObserver((mutations) => {
mutations.forEach(m => {
console.trace('Mutation:', m.type, m);
});
}).observe(target, { childList: true, attributes: true, characterData: true, subtree: true });
ResizeObserver (Related API)
For watching element size changes (not DOM mutations):
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
console.log(`${entry.target.id}: ${width}x${height}`);
// Re-render components that depend on container size
});
});
resizeObserver.observe(document.getElementById('container'));
ResizeObserver fires when an element's size changes (CSS changes, browser resize, content changes). Use for components that need to know their own size.
PerformanceObserver (Related API)
For observing browser performance metrics:
const perfObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.startTime);
}
});
});
perfObserver.observe({ type: 'largest-contentful-paint', buffered: true });
Used for measuring Core Web Vitals (LCP, FID, CLS) in production.
Top comments (0)