How does the Mutation Observer API work?
Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.
Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.
The MutationObserver API provides a way to watch for changes to the DOM tree, replacing the deprecated mutation events.
Basic usage:
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Type:', mutation.type);
console.log('Target:', mutation.target);
});
});
const config = {
attributes: true,
childList: true,
subtree: true
};
const targetNode = document.getElementById('observed');
observer.observe(targetNode, config);
// Later: stop observing
observer.disconnect();
Observing attribute changes:
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes') {
const oldValue = mutation.oldValue;
const newValue = mutation.target.getAttribute(mutation.attributeName);
console.log(`${mutation.attributeName}: ${oldValue} → ${newValue}`);
}
});
});
observer.observe(element, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class', 'data-status']
});
Observing child nodes:
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
console.log('Added:', node);
});
mutation.removedNodes.forEach(node => {
console.log('Removed:', node);
});
});
});
observer.observe(container, {
childList: true,
subtree: true
});
Practical example - lazy loading images when added to DOM:
const imageObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.tagName === 'IMG' && node.dataset.src) {
loadImage(node);
}
// Check descendants
if (node.querySelectorAll) {
node.querySelectorAll('img[data-src]').forEach(loadImage);
}
});
});
});
function loadImage(img) {
img.src = img.dataset.src;
delete img.dataset.src;
}
imageObserver.observe(document.body, {
childList: true,
subtree: true
});
Observing text content changes:
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'characterData') {
console.log('Text changed:', mutation.target.textContent);
}
});
});
observer.observe(textNode, {
characterData: true,
characterDataOldValue: true
});
Configuration options:
const config = {
attributes: true, // Watch attribute changes
attributeOldValue: true, // Record old attribute values
attributeFilter: ['class'], // Only watch specific attributes
childList: true, // Watch child nodes
subtree: true, // Watch all descendants
characterData: true, // Watch text content
characterDataOldValue: true // Record old text content
};
Candidate Response Strategy & Interview Tips
- Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
- Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
- Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
- Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.