Getting to Know Mutation Observers
We used to do this with mutation events. Introduced by the DOM, Level 2 specification, the MutationEvent interface defined several events — such as
DOMAttrModified — that would be fired by the browser when a node was added, removed, or deleted. Mutation events, however, are not without their problems.
The problem with MutationEvents
Though an excellent idea in theory, in practice, mutation events had two major hurdles.
- MutationEvents are synchronous. Events are fired when called, and may prevent other events in the queue from being fired. Add or remove enough nodes, and the application could lag or hang.
Sounds messy, right?
Indeed, mutation events are messy enough to have been deprecated in the DOM, Level 3 specification. But if mutation events are deprecated, we need something to replace them. That’s where mutation observers come in.
How Mutation Observers are different
Mutation observers are defined by the DOM Standard, and differ from mutation events one key way: they are asynchronous. They do not fire every time an event occurs. Instead they:
- Wait until other scripts or tasks complete;
- Report changes in a batch as an array of mutation records, rather than one-by-one; and
- Can observe all changes to a node, or only observe specific kinds of changes.
What’s more, because they are not events, they don’t come with the implementation overhead of events. They’re less likely to freeze the UI or cause a browser crash as a result.
Let’s consider an example. In the code below, we’re appending 2500 paragraphs to a document fragment, and then adding that fragment as a child of an article element.
Even though we’re adding 2500 paragraphs nodes, we’ve batched them into one DOM update by using a document fragment. Still, this bit of code generates 2500
DOMNodeInserted events, one for each paragraph. Our DOMNodeInserted event handler is invoked 2500 times. With a mutation observer, on the other hand, our callback is invoked once. One mutation observer can record multiple DOM operations.
Okay, but can I use them now?
Support for isn’t available everywhere just yet. Opera 15+, Firefox 14+ and Chrome 26+ support the
MutationObserver interface. Internet Explorer 11 will also have support when it’s released, as will Safari 6.1. Safari 6.0 and Chrome versions 18 through 25 also support
MutationObserver, but with a WebKit prefix (
WebKitMutationObserver). You can detect support with the code shown below.
Consult the CanIUse.com chart for the current level of support among browsers.
So how do I use
The good news is that mutation observers are easy to use. First create an observer object using the MutationObserver constructor as shown in Figure 3. The constructor requires a single parameter, a callback function.
Our callback function will receive an array of
MutationRecord objects as an argument. Each
MutationRecord object summarizes a change to the node tree. We’ll discuss mutation records in more detail later.
Next, you’ll need to define a node to observe, and determine what kinds of DOM changes you’d like to keep an eye on. For this, we use the
observe method. Its first parameter must be a node, and its second must be a dictionary of options (Figure 4). In the example below, we’ll watch an article element for changes to its children or attributes.
The options parameter may include the following properties and values.
childListtrue or false; observe mutations to the target node’s children.
attributestrue or false; observe changes to the attributes of a target node.
characterDatatrue or false; Observe changes to the data or text content of the target node.
subtreetrue or false; observe mutations to all descendants of the target, including child nodes and “grandchild nodes” (or the child nodes of child nodes).
attributeOldValuetrue or false; if the attributes property is true, and you’d like to capture the value of the attribute before the mutation is recorded.
characterDataOldValuetrue or false; if the characterData property is true, and you’d like to capture the value of the data before the mutation is recorded.
attributeFiltera list of attributes to observe, enclosed in square brackets (example:
characterData property must be included, and set to
true in order to observe a mutation.
To stop observing mutations, use the
disconnect() method (
observer.disconnect()). Using this method prevents further invocation of the callback function. The
takeRecord method (
observer.takeRecord()) clears the record queue. To resume watching mutations, just re-invoke the
I mentioned above that the mutation callback receives an array of mutation records as an argument. Let’s take a look at what a mutation record is.
A mutation record is an object that reports a single change to the document tree. Mutation record objects are defined by the
MutationRecord interface, and contain the following items.
typethe type of mutation observed, either
targetthe node affected by the mutation.
addedNodesa NodeList of elements, attributes, and text nodes added to the tree.
removedNodesa NodeList of elements, attributes, and text nodes removed from the tree.
previousSiblingreturns the previous sibling node, or null if there is no previous sibling.
nextSiblingreturns the next sibling node, or null if there is no next sibling.
attributeNameThe name of the attribute or attributes changed. If
attributeFilteroption was set, it will only return the filtered node.
oldValuethe pre-mutation value in the case of attribute or
Now that we’ve covered the syntax of mutation observers and mutation records, let’s look at some examples.
Observing the addition or removal of child nodes
Observing the addition or removal of child nodes is pretty straightforward. We’ll create a new object and pass a callback. We’ll also observe our document’s body, and all changes to its children. Figure 5 shows how.
Notice that we’ve included the
subtree option, and set it to
true. Doing so captures when children are appended to the document body (example:
document.body.appendChild(el)), and when they are appended to a child of the body (
document.getElementById('my_element').appendChild(el)). If, instead,
false or missing, the observer would only keep track of elements appended to the body.
It’s also possible to observe mutations to document fragments. Just pass the fragment as the first parameter to the
Observing changes to attributes
Observing changes to attributes works much the same way. The main difference is that you must add
'attributes': true to the options dictionary. If you also want to record the previous attribute value, set the
attributeOldValue option to
true (view a demo).
The example above will capture all changes to any attribute of our target element, including deletions. As you can see in the demo, each time the value of an attribute changes, a new mutation record gets added to the queue. But what if we only wanted to observe changes to particular attributes?
Filtering which attributes are observed
We can limit the which attributes we’d like to observe by adding the
attributeFilter property to our options (Figure 7). The value of
attributeFilter must be a comma-separated list of attributes to track, enclosed in square brackets (
Setting that property means that a mutation record will be generated only for changes to the value of the class attribute (view a demo).
To learn more about mutation observers, try the following resources.