MutationObserver API

By  on  

One of my favorite web tricks was using CSS and JavaScript to detect insertion and removal of a DOM node, detailed in Detect DOM Node Insertions with JavaScript and CSS Animations.  The technique and blog post were published during a time when we didn't have a reasonable API for detecting such events.  Nowadays we have MutationObserver, an API made to efficiently detect loads of node operations.  Let's have a look!

Basic MutationObserver API

The MutationObserver API is a bit complicated for me but here's the basic setup:

var observer = new MutationObserver(function(mutations) {
	// For the sake of...observation...let's output the mutation to console to see how this all works
	mutations.forEach(function(mutation) {
		console.log(mutation.type);
	});    
});
 
// Notify me of everything!
var observerConfig = {
	attributes: true, 
	childList: true, 
	characterData: true 
};
 
// Node, config
// In this case we'll listen to all changes to body and child nodes
var targetNode = document.body;
observer.observe(targetNode, observerConfig);

There's a lot to using the MutationObserver, but the breakdown is:

  • Create an instance of MutationObserver with a callback to handle any event thrown its way
  • Create a set of options for the MutationObserver
  • Call the observe method of the MutationObserver instance, passing it the node to listen to (..and its children) and the option list.
  • At the time you want to stop observing, call disconnect

MutationObserver Options

MDN provides detail on the options for MutationObserver:

  • childList: Set to true if additions and removals of the target node's child elements (including text nodes) are to be observed.
  • attributes: Set to true if mutations to target's attributes are to be observed.
  • characterData Set: to true if mutations to target's data are to be observed.
  • subtree: Set to true if mutations to not just target, but also target's descendants are to be observed.
  • attributeOldValue: Set to true if attributes is set to true and target's attribute value before the mutation needs to be recorded.
  • characterDataOldValue: Set to true if characterData is set to true and target's data before the mutation needs to be recorded.
  • attributeFilter: Set to an array of attribute local names (without namespace) if not all attribute mutations need to be observed.

That's a lot to be aware of when listening to one node and/or child nodes!

MutationRecord:  MutationObserver Handler Results

The resulting object when a mutation is observed is detailed as well:

  • type (String): Returns attributes if the mutation was an attribute mutation, characterData if it was a mutation to a CharacterData node, and childList if it was a mutation to the tree of nodes.
  • target (Node): Returns the node the mutation affected, depending on the type. For attributes, it is the element whose attribute changed. For characterData, it is the CharacterData node. For childList, it is the node whose children changed.
  • addedNodes (NodeList): Return the nodes added. Will be an empty NodeList if no nodes were added.
  • removedNodes (NodeList): Return the nodes removed. Will be an empty NodeList if no nodes were removed.
  • previousSibling (Node): Return the previous sibling of the added or removed nodes, or null.
  • nextSibling (Node): Return the next sibling of the added or removed nodes, or null.
  • attributeName (String): Returns the local name of the changed attribute, or null.
  • attributeNamespace (String): Returns the namespace of the changed attribute, or null.
  • oldValue (String): The return value depends on the type. For attributes, it is the value of the changed attribute before the change. For characterData, it is the data of the changed node before the change. For childList, it is null.

Whew.  So let's take look at some realistic use cases of MutationObserver.

Detect When a Node is Inserted

The use case in my Detect DOM Node Insertions with JavaScript and CSS Animations post was detecting node insertion, so let's create a snippet which detects node insertion:

// Let's add a sample node to see what the MutationRecord looks like
// document.body.appendChild(document.createElement('li'));

{
	addedNodes: NodeList[1], // The added node is in this NodeList
	attributeName: null,
	attributeNamespace: null,
	nextSibling: null,
	oldValue: null,
	previousSibling: text,
	removedNodes: NodeList[0],
	target: body.document,
	type: "childList"
}

The resulting MutationRecord shows addedNodes: NodeList[1], meaning that a node has been added somewhere lower in the tree.  The type is childList.

Detect When a Node is Removed

Removing a node shows the following MutationRecord:

// Now let's explore the MutationRecord when a node is removed
// document.body.removeChild(document.querySelector('div'))

{
	addedNodes: NodeList[0],
	attributeName: null,
	attributeNamespace: null,
	nextSibling: text,
	oldValue: null,
	previousSibling: null,
	removedNodes: NodeList[1], // The removed node is in this NodeList
	target: body.document,
	type: "childList"
}

This action also shows a type of childList but now removeNodes now has the NodeList[1], a NodeList with the removed node.

Detect Attribute Changes

If an attribute is changed on any element, you'll be quick to know about it; the MutationRecord will show:

// What do attribute changes look like?
// document.body.setAttribute('id', 'booooody');

{
	addedNodes: NodeList[0],
	attributeName: "id",
	attributeNamespace: null,
	nextSibling: null,
	oldValue: null,
	previousSibling: null,
	removedNodes: NodeList[0],
	target: body#booooody.document,
	type: "attributes"
}

Also note that the target will show the node for which the attributes was changed.  The oldValue will display its former value and while a normal getAttribute check give you the new attributes value.

Stop Listening!

If you're looking to write the ultimate efficient app, you'll only be adding listeners for the period you need them and then removing them when you're done:

observer.disconnect();

The MutationObserver instance has a disconnect method to stop listening.  Since your app may have many, many DOM operations, you may want the power to disconnect your listener for the duration of the time your user interacts with the page.

The MutationObserver API seems a tad verbose but it's powerful, informative, and ultimately hack free.  Daniel Buchner's brilliant original "hack" provides better support for node addition and removal but MutationObserver should probably be used if possible.

Recent Features

  • By
    Chris Coyier’s Favorite CodePen Demos II

    Hey everyone! Before we get started, I just want to say it's damn hard to pick this few favorites on CodePen. Not because, as a co-founder of CodePen, I feel like a dad picking which kid he likes best (RUDE). But because there is just so...

  • By
    Vibration API

    Many of the new APIs provided to us by browser vendors are more targeted toward the mobile user than the desktop user.  One of those simple APIs the Vibration API.  The Vibration API allows developers to direct the device, using JavaScript, to vibrate in...

Incredible Demos

  • By
    Element Position Swapping Using MooTools 1.2

    We all know that MooTools 1.2 can do some pretty awesome animations. What if we want to quickly make two element swap positions without a lot of fuss? Now you can by implementing a MooTools swap() method. MooTools 1.2 Implementation MooTools 1.2 Usage To call the swap...

  • By
    dwProgressBar v2:  Stepping and Events

    dwProgressBar was a huge hit when it debuted. For those of you who didn't catch my first post, dwProgressBar is a MooTools 1.2-based progress bar which allows for as much flexibility as possible. Every piece of dwProgressBar can be controlled by CSS...

Discussion

  1. Do you know of a way to use MutationObserver to detect when an element’s dimension (width or height) changes? The best cross-browser solution I’ve seen so far is Cross-Browser, Event-based, Element Resize Detection, but that was published two years ago and I wonder if someone has come up with a better solution since.

  2. dharini

    Hi,
    I am using mutation observer on a target node. It fires if any of the child nodes or properties changes.But, I have a case where i had to handle if the target node itself is removed. This is not working.Is there a way to handle this?

  3. Guy Thomas

    I wondered what the performance difference would be between the CSS animation trick and the MutationObserver API.

    So I created a rough jsbin to test the two approaches:

    http://jsbin.com/hibeji

    MutationObserver is consistently faster.

  4. your overview regarding MutationObserver is pretty neat, thanks for that!

    one thing though bugs me: why does the property change “checked” on an input field not get tracked?

    i’ve created a fiddle to illustrate this: enabling/disabling the input field gets tracked fine but checking/unchecking the input field does not.

    https://jsfiddle.net/nerdess/5pc5jLxm/2/

    this makes no sense to me, maybe someone can explain what is going on here…thanks!

    • Stuart Simon

      You don’t use MutationObservers for that purpose. You use the onchange event.

      https://www.w3schools.com/jsref/event_onchange.asp

    • the onchange event does not get triggered though when the state of the input (checked/unchecked) changes programmatically. it only works if a user clicks on the input…

  5. Nice example code, just about the only example online I could find that actually worked correctly. Thanks.

  6. Paul

    Thanks so much for this post!

  7. Very helpful post that actually helps me understand what’s going on with MutationObserver! The only hiccup is that after executing the code provided, I was only seeing the text “childList” in the console. It didn’t take any effort at all to realize that the sample was logging “mutation.type” instead of “mutation”. Made the quick change (in order to observe the records!) and I was off to the races.

    Thanks for the article, David!

  8. oliver

    Maybe i’m being dense…but in what sort of situations would you actually need to use this? Won’t it be my own functions that are changing the DOM in the first place?

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!