DOM updates can arrive from fetch responses, third-party widgets, and component renders long after the initial HTML has loaded. MutationObserver lets browser JavaScript watch a specific part of the DOM and react only when matching child, attribute, or text changes occur.
A focused observer starts with one target node and a small options object. The callback receives MutationRecord entries after the browser batches matching changes, so code can read the changed type, target, added nodes, attribute name, and previous value without polling the page.
Keep the observation scope as narrow as the feature allows. Watching one container and a filtered attribute keeps callbacks predictable, and calling disconnect() after the needed change prevents later page updates from running stale observer code.
Steps to watch DOM changes with MutationObserver:
- Add a target element, controls, and log area to the page.
- index.html
<section id="activity-feed" data-state="idle" aria-live="polite"> <p>No updates yet.</p> </section> <button type="button" id="add-update">Add update</button> <button type="button" id="after-disconnect">Change after disconnect</button> <pre id="mutation-output">No mutation records yet.</pre>
The data-state attribute gives the observer a small state change to filter, while the paragraph insertion proves childList mutations.
- Select the watched element and expose a log for browser-console checks.
- observer.js
const feed = document.querySelector("#activity-feed"); const output = document.querySelector("#mutation-output"); const mutationLog = []; window.mutationLog = mutationLog; function renderMutationLog() { output.textContent = JSON.stringify(mutationLog, null, 2); }
Load observer.js after the target markup or use defer so the selectors can find the elements.
Related: How to load JavaScript with defer - Create the MutationObserver callback.
- observer.js
const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { mutationLog.push({ type: mutation.type, target: mutation.target.id || mutation.target.nodeName.toLowerCase(), addedNodes: mutation.addedNodes.length, attributeName: mutation.attributeName, oldValue: mutation.oldValue }); } renderMutationLog(); if (feed.dataset.state === "ready") { observer.disconnect(); } });
disconnect() stops future callbacks after the expected state is reached. Call observe() again later only when the same observer should start watching again.
- Start observing child and state-attribute changes on the target.
- observer.js
observer.observe(feed, { childList: true, attributes: true, attributeFilter: ["data-state"], attributeOldValue: true });
observe() must enable at least one of childList, attributes, or characterData. attributeFilter limits the attribute records to data-state, and attributeOldValue records the previous value.
- Add the button handler that changes the watched target.
- observer.js
document.querySelector("#add-update").addEventListener("click", () => { const item = document.createElement("p"); item.textContent = "Profile data loaded."; feed.append(item); feed.dataset.state = "ready"; });
The append creates a childList record, and the data-state assignment creates an attributes record in the same observer callback batch.
- Add the post-disconnect test handler.
- observer.js
document.querySelector("#after-disconnect").addEventListener("click", () => { const item = document.createElement("p"); item.textContent = "This change happens after disconnect."; feed.append(item); feed.dataset.state = "ignored"; });
- Click Add update in the browser.
The visible log should show one childList record and one data-state attribute record.
- Check the visible mutation log.
[ { "type": "childList", "target": "activity-feed", "addedNodes": 1, "attributeName": null, "oldValue": null }, { "type": "attributes", "target": "activity-feed", "addedNodes": 0, "attributeName": "data-state", "oldValue": "idle" } ] - Click Change after disconnect in the browser.
- Verify the log length stays unchanged.
> mutationLog.length 2
The target receives the extra DOM change, but the disconnected observer no longer appends mutation records.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.