MutationObserver Records and Subtree Delivery
MutationObserver batches records for microtask delivery instead of promising one callback per change. Learn what subtree and option combinations actually mean.
Publication & corrections: infostoa · Editorial standards
Individual author and specialist reviewer information is not provided for this article.
MutationObserver records DOM changes as MutationRecord objects and notifies pending observers in a microtask. Do not assume one callback for every individual change. The DOM Standard defines queuing and delivery; MDN’s observe() reference documents the options.
Conceptual illustration, not a browser run.
Options define what is observed
Call observer.observe(target, options) with the change kinds you need: childList, attributes, or characterData. subtree:true extends that observation to descendants. Enabling subtree without an effective mutation type can produce an invalid configuration. One observer can watch multiple targets; observing the same target again updates its options.
Records are delivered in batches
The standard queues a mutation-observer microtask and avoids queueing another while one is already pending for the agent. The callback receives records removed from the queue at delivery time. The number of changes therefore does not map one-to-one to callback count. Mutations made inside the callback can create more records and schedule a later delivery. There is no promise of one callback per rendering frame or a fixed delay.
What happens when a descendant is removed
While subtree observation is active, changes inside a removed descendant can still be captured until the removal record is delivered. The standard uses transient registered observers for this interval, then removes those temporary registrations during notification. This does not mean a detached node remains observed indefinitely. Another observer registered directly on that node is a separate case.
Keep only useful records
Choose attributes, characterData, and childList for the work you intend to do; use attributeFilter to narrow attribute changes. Set attributeOldValue only when the previous value matters. Combining it with attributes:false is contradictory, while supplying an old-value option or a filter can enable attribute observation. Apply the same check to characterDataOldValue. Call disconnect() when observation is no longer needed, and use takeRecords() first if pending records matter. Use record targets and kinds to limit follow-up work instead of rescanning an entire DOM tree for every delivery.
Configure options and inspect each record
If a list only needs child additions and removals, make the intent explicit: observer.observe(list, {childList: true, subtree: true}). To capture attribute changes and their prior values, set both attributes: true and attributeOldValue: true; narrow the set with attributeFilter: ['class', 'aria-expanded']. Supplying a filter or an old-value option can enable attribute observation when attributes is omitted, so explicit options make review easier. A contradictory pair such as attributes: false and attributeOldValue: true throws. Apply the same consistency check to characterDataOldValue and characterData. MDN’s observe options lists the defaults and invalid combinations.
A MutationRecord reference describes fields such as type, target, addedNodes, removedNodes, attributeName, and oldValue. Branch on record.type and handle only the targets the feature needs instead of diffing a large subtree on every callback. A record is a change notification, not a snapshot of the entire DOM before and after the change. Without requesting oldValue, the previous value is not available from the record; by callback time, referenced nodes may also have changed again.
If pending records must be consumed synchronously before a state transition, takeRecords() returns and clears the queue. disconnect() stops observation, but it does not automatically reconcile unhandled changes into application state. Decide whether to process or discard takeRecords() before teardown. Conversely, rereading the queue while handling records already delivered to the callback can apply the same logical change twice.
Expert perspective: notifications are not state
MDN contributors note that a removed subtree can remain observable until its removal record is delivered. The practical interpretation is not to treat MutationObserver as a durable state store: distinguish callback records from the current DOM and keep application state elsewhere. That separation reduces missed or duplicate reconciliation around removal and reconnection boundaries.