Why observe the DOM?

A page can change after its initial load. A component might insert a notification, replace search results, or update an attribute that controls its appearance. If your code does not control the change, repeatedly checking the page with a timer is tempting. That approach does work in some cases, but it makes checks even when nothing has changed and forces you to choose a polling interval.

MutationObserver provides a way to be notified about specific kinds of DOM changes. You choose a node to watch and describe the changes that matter. The browser then delivers mutation records to your callback. This is useful when integrating with code that updates a part of the page independently, such as a reusable widget. It is not a substitute for ordinary component state: when your own code makes a change, handling it at the point where it happens is usually simpler.

A small, reproducible example

This example runs in a modern browser with JavaScript enabled. Put an element with id="messages" and a button with id="add-message" in an HTML page. Run the following JavaScript after those elements exist, for example from a script at the end of the body. It watches for children being added directly to the messages element.

const messages = document.querySelector('#messages'); const button = document.querySelector('#add-message'); const observer = new MutationObserver((records) => { for (const record of records) { for (const node of record.addedNodes) { if (node instanceof Element && node.matches('.message')) { console.log('New message:', node.textContent); } } } }); observer.observe(messages, { childList: true }); button.addEventListener('click', () => { const item = document.createElement('p'); item.className = 'message'; item.textContent = 'A new message'; messages.append(item); });

Clicking the button appends a paragraph, and the observer logs it. The Element check matters because addedNodes can contain text nodes as well as elements. This example assumes both queried elements exist; in a larger page, check for null before using them. The observer reports a change rather than a user action, so it would also notice a matching paragraph added by other JavaScript.

Choose the changes you need

The options passed to observe determine what can produce records. childList watches for children being added or removed. attributes watches for attribute changes on the observed node; attributeFilter can narrow that to names such as ['aria-expanded']. characterData watches for changes to the data in text nodes. Add subtree: true when changes in descendants should count too. For example, observer.observe(messages, { childList: true, subtree: true }) also watches child-list changes within nested elements.

Start with the smallest relevant container and the fewest options that meet your need. Watching the entire document with subtree: true can send your callback many unrelated records. Mutation records identify details such as the changed node and mutation type; child-list records also expose addedNodes and removedNodes. If you need a previous attribute value, request attributeOldValue: true along with attribute observation. An observer does not report what was already in the DOM before observation began, so inspect existing content separately if the initial state matters.

Understand callback timing and cleanup

MutationObserver callbacks do not run in the middle of each DOM-changing operation. Records are delivered asynchronously, often as a batch after the current JavaScript work completes. Code should therefore use the records to find relevant changes rather than assume that one callback corresponds to one edit. By callback time, the page may have changed again; a removed node, for example, may no longer be connected to the document.

Keep a reference to the observer so you can stop it when the feature is removed: observer.disconnect(). This is especially important for a widget that can be mounted and unmounted repeatedly. If you need to retrieve records that have been queued but not yet delivered, observer.takeRecords() returns them and clears that queue. Disconnecting does not perform that processing for you. Decide whether pending changes matter before tearing down the observer.

Avoid unnecessary work and feedback loops

An observer callback can change the DOM itself. If that change matches the observer’s options, it may trigger another callback. For example, watching all attributes in a container and setting an attribute on every notification can create repeated work. Filter records first, make updates only when the desired state is not already present, or observe a narrower target. If a larger rewrite is unavoidable, disconnecting and then observing again may be appropriate, but changes made while disconnected will not be reported.

Use MutationObserver for DOM mutations, not every kind of change on a page. It does not tell you that an element has become visible because the viewport moved or that its size changed. When your goal is to respond to those conditions, choose a browser API suited to visibility or size instead. A focused observer, paired with an initial state check and explicit cleanup, is a reliable alternative to polling when DOM changes are what you actually need to detect.

Back to category ↗