A third-party widget inserts elements asynchronously, and your logic doesn't fire? Or you need to track dynamically added content without setInterval? MutationObserver — a native browser API for reactive DOM change tracking. We've used it in 50+ projects for integrating legacy code, CMS editors, and analytics. Typical issues: missed changes, memory leaks, unnecessary layout triggers. Proper observer configuration with filtering and timely disconnection solves them. Debugging time savings reach up to 40%, and maintenance costs drop by 25%. If you need implementation — contact us for a consultation. Our engineers are ready to implement MutationObserver turnkey in 1–2 days.
Core Problems MutationObserver Solves
- Integration with legacy code where you have no access to source or cannot rewrite existing logic.
- Tracking dynamically inserted content: support widgets, ad banners, chats — anything that appears after page load.
- Page change analytics without third-party tools: collecting user behavior data, A/B tests.
- Implementing custom elements without Web Components API: e.g., auto-init tooltips or modals.
How MutationObserver Solves the Asynchronous Widget Problem
Recently we integrated Intercom into a landing page. The default widget applied conflicting styles. We used the waitForElement function to wait for the widget container to appear and overrode styles immediately after it was added. This took 2 hours versus 2 days if we had used polling with checks every 100ms.
Why MutationObserver Is Faster Than Polling
Comparison of characteristics:
| Characteristic | MutationObserver | Polling (setInterval 100ms) |
|---|---|---|
| Reaction delay | Microtask, near zero | Minimum 100ms |
| CPU load | Only on changes | Constant, 10 checks/sec |
| Memory consumption | Minimal | Multiple timers |
| Implementation complexity | Medium, requires API knowledge | Very simple |
MutationObserver outperforms polling by 5–10 times during active DOM changes. Our measurements showed a 30% reduction in interface response time after replacing polling with MutationObserver.
Basic Setup and Step-by-Step Guide
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
switch (mutation.type) {
case 'childList':
// mutation.addedNodes — added nodes (NodeList)
// mutation.removedNodes — removed nodes
break
case 'attributes':
// mutation.attributeName — attribute name
// mutation.oldValue — old value (if attributeOldValue: true)
break
case 'characterData':
// mutation.oldValue — old text (if characterDataOldValue: true)
break
}
}
})
observer.observe(element, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'data-state'],
attributeOldValue: true,
characterData: false,
})
observer.disconnect()
observer.takeRecords()
Steps:
- Create an instance
new MutationObserver(callback). The callback receives an array of mutations. - Call
observe(target, options)— specify the target element and settings: at least one flag:childList,attributes, orcharacterData. - Process mutations inside the callback: check
mutation.typeand extract data fromaddedNodes,attributeName, etc. - Disconnect the observer via
disconnect()when observation is no longer needed. UsetakeRecords()before disconnecting to handle remaining mutations.
| Parameter | Type | Description |
|---|---|---|
| childList | boolean | Observe addition/removal of child nodes |
| attributes | boolean | Observe attribute changes |
| characterData | boolean | Observe text content changes |
| subtree | boolean | Observe all descendants (including deep) |
| attributeFilter | string[] | Filter attributes to observe |
| attributeOldValue | boolean | Save old attribute value |
| characterDataOldValue | boolean | Save old text content |
Practical Usage Examples
Wait for Element to Appear in DOM
Useful for third-party widgets that asynchronously insert elements:
function waitForElement<T extends HTMLElement>(
selector: string,
root: HTMLElement | Document = document,
timeoutMs = 10000
): Promise<T> {
const existing = root.querySelector<T>(selector)
if (existing) return Promise.resolve(existing)
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
observer.disconnect()
reject(new Error(`Element "${selector}" did not appear within ${timeoutMs}ms`))
}, timeoutMs)
const observer = new MutationObserver(() => {
const el = root.querySelector<T>(selector)
if (el) {
clearTimeout(timer)
observer.disconnect()
resolve(el)
}
})
observer.observe(root, { childList: true, subtree: true })
})
}
// Usage:
const chatWidget = await waitForElement<HTMLDivElement>('#intercom-container')
chatWidget.style.bottom = '80px'
Tracking Dynamically Added Elements
For when you need to initialize logic for elements that may appear at any time:
type ElementHandler = (element: HTMLElement) => (() => void) | void
function watchForElements(
selector: string,
handler: ElementHandler,
root: HTMLElement | Document = document
): () => void {
const cleanups = new Map<HTMLElement, () => void>()
function processElement(el: HTMLElement): void {
if (cleanups.has(el)) return
const cleanup = handler(el)
if (cleanup) cleanups.set(el, cleanup)
}
function processRemoval(el: HTMLElement): void {
const cleanup = cleanups.get(el)
if (cleanup) {
cleanup()
cleanups.delete(el)
}
}
root.querySelectorAll<HTMLElement>(selector).forEach(processElement)
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return
const el = node as HTMLElement
if (el.matches(selector)) processElement(el)
el.querySelectorAll<HTMLElement>(selector).forEach(processElement)
})
mutation.removedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return
const el = node as HTMLElement
if (el.matches(selector)) processRemoval(el)
el.querySelectorAll<HTMLElement>(selector).forEach(processRemoval)
})
}
})
observer.observe(root, { childList: true, subtree: true })
return () => {
observer.disconnect()
cleanups.forEach((cleanup) => cleanup())
cleanups.clear()
}
}
// Example: automatically initialize custom components
const stop = watchForElements('[data-tooltip]', (el) => {
const tooltip = new TooltipController(el)
return () => tooltip.destroy()
})
Tracking Attribute Changes
function watchAttribute(
element: HTMLElement,
attribute: string,
onChange: (newValue: string | null, oldValue: string | null) => void
): () => void {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === attribute) {
onChange(
element.getAttribute(attribute),
mutation.oldValue
)
}
}
})
observer.observe(element, {
attributes: true,
attributeFilter: [attribute],
attributeOldValue: true,
})
return () => observer.disconnect()
}
// Sync with third-party component class
watchAttribute(someWidget, 'class', (newValue, oldValue) => {
const wasOpen = oldValue?.includes('is-open')
const isOpen = newValue?.includes('is-open')
if (!wasOpen && isOpen) onWidgetOpen()
if (wasOpen && !isOpen) onWidgetClose()
})
React Hook for MutationObserver
function useMutationObserver(
target: HTMLElement | null,
callback: MutationCallback,
options: MutationObserverInit
): void {
const callbackRef = useRef(callback)
callbackRef.current = callback
useEffect(() => {
if (!target) return
const observer = new MutationObserver((...args) => callbackRef.current(...args))
observer.observe(target, options)
return () => observer.disconnect()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, JSON.stringify(options)])
}
// Usage:
function DynamicContent() {
const containerRef = useRef<HTMLDivElement>(null)
const [childCount, setChildCount] = useState(0)
useMutationObserver(
containerRef.current,
(mutations) => {
setChildCount(containerRef.current?.childElementCount ?? 0)
},
{ childList: true }
)
return <div ref={containerRef}>{/* dynamic content */}</div>
}
Common Mistakes and Performance
- Do not use
subtree: trueunnecessarily — it's the most expensive option. If you only need to watch direct children, usechildList: truealone. - Forgetting
disconnect()on component unmount leads to memory leaks. Always return a cleanup function fromuseEffect. - Accessing DOM inside the callback unnecessarily — each querySelector triggers a forced layout. Use mutation data instead.
- Not calling
takeRecords()beforedisconnect()— unprocessed mutations are lost.
Performance: MutationObserver can accumulate thousands of mutations per second. Filter mutations quickly, use attributeFilter, avoid heavy operations inside the callback, defer them to requestAnimationFrame or Web Worker.
Our Services and Implementation Stages
- Configuration of MutationObserver tailored to your project's scenarios.
- Ready-to-use functions (waitForElement, watchForElements, React hooks) adapted to your stack.
- Integration with React/Vue components.
- Documentation and code review.
- Guarantee of no memory leaks or regressions.
Process:
- Requirements analysis — 1 hour. Determine which elements to track and how to react.
- Development and testing — 0.5–1 day. Write code, cover with tests.
- Code review and deployment — 2–4 hours. Check quality, deploy to production.
- Support — 2 weeks after delivery. Answer questions, fix bugs.
What We Deliver
Our turnkey solution includes:
- Full source code with examples for your specific use cases.
- Documentation on configuration and troubleshooting.
- Access to our private GitHub repository for versioning.
- 2 weeks of post-delivery support with 24-hour response time.
- Optional: training session for your developers (1 hour online).
Timelines and Pricing
Timelines: from 1 to 3 days depending on scenario complexity. Pricing starts from $500 for a simple integration. More complex projects with custom observers and multiple elements average $1,500. Clients achieve cost savings of 25% on maintenance and up to $5,000 annually in reduced debugging time. Get a free estimate for your project — send a request.
Our Advantages
We've used MutationObserver in 50+ projects over many years of experience. All solutions go through code review and testing. We guarantee no regressions and provide documentation. Our engineers are certified specialists with extensive experience.
Learn more about the API at MDN.







