Suggest an editImprove this articleRefine the answer for “MutationObserver”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`MutationObserver` is a built-in browser API that watches for DOM changes: added and removed child nodes, attribute changes and text changes, and reports them through a callback.** You create an observer with a callback, call `observe(target, options)` and switch on what you care about in the options: `childList`, `attributes`, `characterData`, `subtree`. The callback receives an array of `MutationRecord` objects describing each change. It works asynchronously, on the microtask queue, and delivers changes in a batch, so it does not block the UI. It is the modern replacement for the deprecated mutation events `DOMNodeInserted` and `DOMSubtreeModified`, which slowed pages down. ```javascript const observer = new MutationObserver((mutations) => { mutations.forEach((m) => console.log('Change type:', m.type)); }); observer.observe(document.querySelector('#app'), { childList: true, attributes: true, subtree: true, }); ``` **Key point:** `MutationObserver` is an asynchronous "DOM guard": `observe()` starts watching, `disconnect()` stops it, and `takeRecords()` drains the pending records.Shown above the full answer for quick recall.Answer (EN)Image**`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 - `MutationObserver` watches 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`, plus `subtree` for the whole subtree. - The callback receives an array of `MutationRecord` objects, 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 `DOMNodeInserted` and `DOMSubtreeModified` events. ### Quick example ```javascript // 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 ```javascript <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 ```javascript 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: ```javascript observer.disconnect(); ``` To drain the changes collected so far and, if needed, start watching again: ```javascript observer.takeRecords(); // returns the accumulated changes observer.observe(target, { childList: true }); // can be switched on again ``` `takeRecords()` 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): ```javascript 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: ```javascript 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 - `MutationObserver` works 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: true` on the whole `document`, 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 the `childList`, `attributes`, `characterData` flags.** The browser throws a `TypeError`, 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, use `takeRecords()`. - **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, then `observe()` again. - **Putting `subtree: true` on `document.body` for the sake of one button.** The observer will wake the callback on every tiny page change. Narrow the `target` and add an `attributeFilter`. - **Forgetting `disconnect()`.** The observer keeps references to the node and the callback, so tearing down a component without disconnecting leaks memory. - **Confusing it with `IntersectionObserver` or `ResizeObserver`.** The first watches element visibility, the second its size, while `MutationObserver` only watches DOM structure and attributes.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.