MutationObserver
MutationObserver is a special object that watches an element and reports when something inside it has changed: structure, attributes or text. It works asynchronously, does not block the UI, and invokes a callback when changes happen in the node being observed.
Theory
TL;DR
MutationObserverwatches DOM changes and lets you react to them in real time.- You create it with a callback and start it with
observer.observe(target, options). - The options switch on the change types:
childList,attributes,characterData, plussubtreefor the whole subtree. - The callback receives an array of
MutationRecordobjects, one record per change. - It runs asynchronously through the microtask queue and delivers changes in a batch, so it is cheap.
- Control:
observe(),disconnect(),takeRecords(). - It replaces the deprecated
DOMNodeInsertedandDOMSubtreeModifiedevents.
Quick example
// 1. Create the observer
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log('Change type:', mutation.type);
});
});
// 2. The element we are going to watch
const target = document.querySelector('#app');
// 3. Observation options
observer.observe(target, {
childList: true, // watch adding and removing child elements
attributes: true, // watch attribute changes
subtree: true // watch everything inside the element
});Now, if you add a new element to #app or change an attribute, a message with the change type appears in the console.
A live example
<div id="box">Hello!</div>
<button id="btn">Change</button>
<script>
const box = document.getElementById('box');
const btn = document.getElementById('btn');
const observer = new MutationObserver((mutations) => {
mutations.forEach((m) => console.log(m));
});
observer.observe(box, { childList: true, attributes: true, characterData: true });
btn.addEventListener('click', () => {
box.textContent = 'Text changed!';
box.setAttribute('data-status', 'updated');
});
</script>On click the console shows MutationRecord objects that carry information about:
- the change type (
attributes,childList,characterData); - which attribute changed;
- the old and the new value, if that was enabled.
The observe() options
observer.observe(target, {
childList: true, // adding and removing child elements
attributes: true, // attribute changes
characterData: true, // text changes inside nodes
subtree: true, // watch descendants (the whole tree)
attributeFilter: ['class', 'style'], // watch only these attributes
attributeOldValue: true, // keep the previous attribute value
characterDataOldValue: true // keep the previous text value
});If none of childList, attributes or characterData is enabled, the browser throws: the observer has nothing to watch.
What a MutationRecord contains
Every item of the mutations array is a MutationRecord object with these properties:
| Property | Description |
|---|---|
type | change type (attributes, childList, characterData) |
target | the element where the change happened |
addedNodes | added elements |
removedNodes | removed elements |
attributeName | name of the changed attribute |
oldValue | previous value of the attribute or the text |
Controlling the observer
To stop watching:
observer.disconnect();To drain the changes collected so far and, if needed, start watching again:
observer.takeRecords(); // returns the accumulated changes
observer.observe(target, { childList: true }); // can be switched on againtakeRecords() hands back the records that have not reached the callback yet and clears the queue. That is useful right before disconnect(), so nothing is lost.
Practical scenarios
A very common case is catching new elements inserted by a JS framework (React, Vue, or a third party script):
const container = document.querySelector('#feed');
const observer = new MutationObserver((mutations) => {
mutations.forEach((m) => {
m.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.matches('.post')) {
console.log('A new post appeared:', node.textContent);
}
});
});
});
observer.observe(container, { childList: true, subtree: true });Now a dynamically added <div class="post">...</div> triggers your JS immediately.
The second typical scenario is reacting automatically to a class change:
const box = document.querySelector('#box');
const observer = new MutationObserver((entries) => {
for (const mutation of entries) {
if (mutation.attributeName === 'class') {
console.log('Class changed to:', box.className);
}
}
});
observer.observe(box, { attributes: true });This is handy for tracking state changes, for example during animations or toggle effects.
Performance and summary
MutationObserverworks asynchronously, changes are batched and delivered together rather than per character or per pixel.- That makes it very efficient even with a lot of observations.
- Still, do not abuse
subtree: trueon the wholedocument, it can get expensive.
| What it does | MutationObserver |
|---|---|
| Tracks | DOM changes (attributes, text, adding and removing elements) |
| Replacement for | The old DOMNodeInserted, DOMSubtreeModified events |
| Runs | Asynchronously, through the microtask queue |
| Used for | Dynamic UI, integrations, watching React or Vue, animations, custom components |
| Control | observe(), disconnect(), takeRecords() |
Common mistakes
- Calling
observe()without any of thechildList,attributes,characterDataflags. The browser throws aTypeError, because there is nothing to observe. - Expecting the callback to run synchronously. Right after
element.setAttribute(...)the callback has not fired yet, changes arrive on a microtask. If you need the result immediately, usetakeRecords(). - Mutating the DOM inside the callback without a guard. Your own changes come back to the observer and an infinite loop is easy to create. The fix:
disconnect()temporarily, mutate, thenobserve()again. - Putting
subtree: trueondocument.bodyfor the sake of one button. The observer will wake the callback on every tiny page change. Narrow thetargetand add anattributeFilter. - Forgetting
disconnect(). The observer keeps references to the node and the callback, so tearing down a component without disconnecting leaks memory. - Confusing it with
IntersectionObserverorResizeObserver. The first watches element visibility, the second its size, whileMutationObserveronly watches DOM structure and attributes.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.