Migrating mutation and property change events to mutation observers

Mutation observers provide developers with a way to detect insertion and removal of a DOM node. You can migrate existing code using mutation events and / or property change events to use mutation observers.

Note  Mutation observers gained support from Internet Explorer 11 forward. They offer a fast-performing replacement for all of the same scenarios supported by the now deprecated mutation events, and an alternative to the scenarios supported by property change events.

 

Legacy techniques for monitoring DOM mutations

Mutation events play a key role in the web platform. They allow web apps to synchronously monitor dynamic changes to elements in the Document Object Model (DOM) of a webpage. While useful, mutation events are also known to cause app performance regressions mainly because of their synchronous nature and the event architecture the work on.

Note   Mutation events (as defined in the W3C DOM Level 3 Events) have been deprecated in favor of mutation observers (W3C DOM4).

 

Property change events provide similar behavior as mutation events. They also carry a performance penalty because of the legacy browser event system they need to function correctly.

Note  The onpropertychange event is only supported with the legacy attachEvent IE-only event registration model, which has been deprecated since Windows Internet Explorer 9 (and discontinued in IE11) in favor of the W3C standard "addEventListener" event model.

 

Identifying mutation events

The mutation events, first available in Internet Explorer 9, can be easily identified by their name, which is a string parameter passed to either the addEventListener or removeEventListener platform APIs:

  • DOMNodeInserted
  • DOMNodeRemoved
  • DOMSubtreeModified
  • DOMAttrModified
  • DOMCharacterDataModified

Note  Two additional mutation events are defined by the standard, but not supported by Internet Explorer: DOMNodeInsertedIntoDocument and DOMNodeRemovedFromDocument.

 

Here's an example of what one of these events might look like in JavaScript code:

someElement.addEventListener("DOMAttrModified", function() {
  //...
}, false);

The DOMNodeInserted, DOMNodeRemoved, and DOMSubtreeModified mutation events monitor structural changes to an element's children—either elements are added to the element's children or they're removed. The DOMSubtreeModified event is for both:it's fired for removals and adds. However, it doesn't contain any information as to why it was fired (you can't distinguish an add from a remove based on the event alone).

The DOMAttrModified mutation event reports changes to an element's attribute list. This single event includes information related to attribute insertions, removals, or changes.

The DOMCharacterDataModified mutation event reports changes to an element's text content. Text content is grouped into logical units called text nodes, and only modifications to an existing text node will fire the DOMCharacterDataModified event. If new text nodes are inserted / created, they're reported as DOMNodeInserted events instead.

It should be simple to find mutation events in your code, like using the Find in Files... search feature of your favorite editor. Remember that variables are often used in the addEventListener method, so be sure to search first for the use of the mutation event strings ("DOMNodeInserted", "DOMNodeRemoved", etc.), and then double-check all occurrences of addEventListener to be sure you've found them all.

Identifying property change events

Property change events can be identified by the onpropertychange event name used along with the legacy attachEvent or detachEvent IE-only event registration APIs. Search for all occurrences of attachEvent and check the first parameter for onpropertychange to find these usages in your code.

The property change event fires when a DOM element's properties change. The event doesn't bubble and has been deprecated since Internet Explorer 9 in favor of the W3C standard "addEventListener" event model. The event includes the name of the property that changed in the events propertyName getter. Unfortunately, to dispatch a property change event, a number of other event attributes are also calculated, some of which force the layout engine to recalculate, causing a substantial performance cost to any application using these events.

Unlike with mutation events, the property change event doesn't cleanly map to mutation observers. However, its possible to replace the usage of property change events with mutation observers if the property names of interest are reflected in HTML attributes. For example, id, which reflects the id attribute, style.color which is reflected in the serialized style attribute, and className which corresponds to the class attribute.

Note  For properties that aren't reflected in HTML attributes (such as value on input elements), you can use the ECMAScript 5 (JavaScript) feature called defineProperty. This document doesn't describe how to migrate property change events using the Object.defineProperty JavaScript API.

 

How mutation observers differ

Mutation observers aren't based on the web platform's event model. This is an important difference that enables them to dispatch much faster and without needing to bubble an event through the DOM element hierarchy.

Additionally, mutation observers are designed to record multiple changes before notifying your observer. They batch mutation records to avoid spamming your app with events. By contrast, mutation events are synchronous and interrupt normal code execution to notify your app of mutations. Despite the delayed notification model employed by mutation observers, your apps's observer is still guaranteed to receive (and have a chance to process) all the mutation records before the next repaint.

Both of these changes impact how your app must be adapted to support mutation observers.

Mutation observer registration

Mutation observers must first be created before they can be registered on a given element. To create a mutation observer, use the JavaScript new operator and specify a callback method:

var mutationObserver = new MutationObserver(callback);

The callback that you provide to the mutation observer constructor will be different than the callback you are likely using for your current mutation events. This will be explained in more detail below.

Having created the observer, you now instruct it to observe a particular element. Generally, this will be the same element on which you were previously registering the mutation event:

mutationObserver.observe(someElement, options);

If you don't save a reference to it, the mutation observer instance will be preserved in-memory by the web platform as long as it is observing at least one element. If you don't save a reference to the observer, you can still reference it from the observer's callback (it will be the this object in the callback's scope, as well as the 2nd parameter to the callback function).

The options parameter is a simple JavaScript object with properties that you must provide to describe exactly what kinds of mutations you want to observe. The property options correspond to the three categories of mutations noted earlier:

  • childList
  • attributes
  • characterData

The childList option with a value of true means observe changes to this element's child elements (both removals and additions). This option includes text nodes that are added or removed as children of this element.

The attribute option with a value of true means observe changes to this element's attributes (both removals, additions, and changes).

The characterData option with a value of true means observe changes to this element's text nodes (changes to the values of text nodes, excluding when text nodes are removed entirely or newly added).

A fourth subtree option is also important. The three previous options (by default) only observe their target element in isolation, not considering any of its descendants (its subtree). To monitor the given element and all its descendants, set the subtree property to true. Because mutation events have the characteristic of bubbling through the DOM, the use of the subtree option is required to maintain parity with mutation events registered on ancestor elements.

The following table describes what mutation observer options correspond to what mutation event names:

Mutation event Mutation observer options Notes
DOMNodeInserted { childList: true, subtree: true } Callback must manually ignore node removal records
DOMNodeRemoved { childList: true, subtree: true } Callback must manually ignore node added records
DOMSubtreeModified { childList: true, subtree: true } Callback can now distinguish between added and removed nodes
DOMAttrModified { attributes: true, subtree: true }
DOMCharacterDataModified { characterData: true, subtree: true }

 

Note  With mutation observers it is also possible to combine multiple options to observe childLists, attributes, and characterData at the same time.

 

Finally, there are several options for saving the previous values of attributes and character data changes, and for refining the scope of which attributes are important to observe:

  • The attributeOldValue and characterDataOldValue options with a value of true save the previous value when changes to attributes or characterData occur.
  • The attributeFilter option with a string array of attribute names limits observation to the specified attributes. This option is only relevant when the attributes option is set to true.

With this information, any code that previously registered for a mutation event can be replaced with code that registers for a mutation observer:

// Watch for all changes to the body element's children
document.body.addEventListener("DOMNodeInserted", nodeAddedCallback, false);
document.body.addEventListener("DOMNodeRemoved", nodeRemovedCallback, false);

Now becomes:

// Watch for all changes to the body element's children
new MutationObserver(nodesAddedAndRemovedCallback).observe(document.body, 
  { childList: true, subtree: true });

Mutation observer callbacks

The mutation observer callback function is invoked with two parameters:

  • A list of records
  • A reference to the mutation observer object that's invoking the callback

Be careful if you're reusing your mutation events callbacks for mutation observers. When a relevant mutation happens, the MutationObserver records the change information you requested in a MutationRecord object and invokes your callback function, but not until all script within the current scope has run. It's possible that more than one mutation (each represented by a single MutationRecord) will occur since the last time the callback was invoked.

The records parameter is a JavaScript array consisting of MutationRecord objects. Each object in the array is representative of one mutation that occurred on the element (or elements) being observed.

A record has the following properties:

MutationRecord property Description

type

The type of mutation this record logged. Possible values: attributes, characterData, childList.

target

The element on which the mutation was logged. Similar to event.target or event.srcElement.

addedNodes, removedNodes

An array of nodes that were added or removed as part of this mutation; only relevant when type is childList. These arrays can be empty.

previousSibling, nextSibling

The previous and next siblings of the added or removed node; only relevant when type is childList. These values can be null.

attributeName, attributeNamespace

The name and namespace of the attribute that was added, removed, or changed. Value will be null if the record type is not attributes.

oldValue

The previous value of the attribute or characterData. The value may be null if the mutation observer options did not include the attributeOldValue or characterDataOldValue flags, or if the type is childList.