You're polling setInterval to detect DOM changes. MutationObserver fires when they happen.
Watching for DOM mutations with setInterval or custom event dispatch is fragile and imprecise. The MutationObserver API delivers a callback exactly when attributes, text content, or child elements change — with full control over which types of mutations you watch.

The DOM can change in ways your code didn't trigger — a third-party widget injects a node, a library adds a class, a rich text editor updates its content. The common response is polling: setInterval(() => checkIfThingChanged(), 100). It works until it doesn't. You tune the interval, miss changes that happen between ticks, and pay CPU cost whether anything changes or not. MutationObserver is the built-in answer.
Two steps: create an observer with a callback, then call .observe(node, options). The callback fires asynchronously after a batch of mutations completes — never mid-update — with an array of MutationRecord objects, one per change.
Here's the typical "wait for a class to appear" pattern using an interval:
The 50 ms interval means you catch changes up to 50 ms late, it burns CPU on every tick even when nothing is happening, and if you forget to clearInterval, it runs indefinitely. With MutationObserver:
No timer, no polling overhead, no missed windows.
Third-party widget lifecycle. When you embed a chat widget or payment form from a third party, you have no hook into when it finishes rendering. MutationObserver on the container gives you that hook without touching the third-party script.
Auto-resizing a textarea. The input event doesn't fire on programmatic changes. Observing characterData on the textarea's text node catches every content change regardless of source:
Accessible live announcements. Screen readers miss DOM updates that don't go through an aria-live region. A MutationObserver on a dynamic feed can forward new content to an announcer element, making additions audible without a framework:
MutationObserver delivers changes in batches. If a loop adds 500 nodes in one synchronous operation, you get one callback with 500 records — not 500 callbacks. This is intentional: the observer is low-overhead even on large, active DOM trees.
A few things to keep in mind:
- Narrow the target. Observing
document.bodywithsubtree: truewatches every node in the page. Prefer the smallest relevant container. - Always disconnect. The observer holds a reference to the target node, which can delay garbage collection. Call
observer.disconnect()when you're done. takeRecords()for early cleanup. This flushes any pending, undelivered records synchronously — useful when you need to process remaining mutations before disconnecting without waiting for the next callback.
MutationObserver is Baseline 2015: Chrome 26 (2013), Firefox 14 (2012), Safari 7 (2013). It has shipped in every major browser and runtime for over a decade, including Web Workers.
Runs right in your browser — poke at it and watch the concept react live.
Think it clicked? Take the 9-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Search your codebase for setInterval calls that check a DOM condition and clear themselves once it's true — that's polling for a DOM change. MutationObserver replaces the pattern with a callback that fires the moment the change occurs, with zero cost in between. It completes the browser's built-in observer trio: IntersectionObserver for element visibility, ResizeObserver for element size, and MutationObserver for everything the DOM itself changes. Once you know all three, most "watch the DOM" problems reduce to picking the right one.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Originally published on dev.to