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

Incredible Demos

  • By
    Create Spinning, Fading Icons with CSS3 and MooTools

    A goal of my latest blog redesign was to practice what I preached a bit more;  add a bit more subtle flair.  One of the ways I accomplished that was by using CSS3 animations to change the display of my profile icons (RSS, GitHub, etc.)  I...

  • By
    9 Mind-Blowing WebGL Demos

    As much as developers now loathe Flash, we're still playing a bit of catch up to natively duplicate the animation capabilities that Adobe's old technology provided us.  Of course we have canvas, an awesome technology, one which I highlighted 9 mind-blowing demos.  Another technology available...

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?

    • Daniel Li

      You can observe the parent of your “target node”. Then, in the MutationRecord that is passed to the callback, check the removedNodes to see if your “target node” is there.

  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?

    • Daniel Li

      A common use case for this would be for a contenteditable element, where the user can directly alter the DOM tree.

  9. bojan

    **Oliver** , you would use this constructor if you are adding your project to some 3rd party library or some already made site/app and you want to know when something changes in DOM. Knowing this can help you to trigger your functions and add your changes.

    Example:

    you are waiting for clients page to make some change (lets say function that waits for XHR response in form of JSON that adds some new piece to DOM structure) , by knowing when it was triggered you are able to append your changes after knowing it will not be overwritten.

    Lets, say you want to add a ‘click’ event to some button, but that button gets changed in process of loading because some other library is making changes after page load, so you can wait for this change and on it add your code.

    P.S. this is how I see usage of observer

  10. Taylor Schaerer

    Hello, thanks for the informative post! Unfortunately, Object.observe has now been deprecated in favor of using Proxy which makes this a bit obsolete. I am only just coming to grips with this change myself, but I’d very much be interested in your ideas on how best to use Proxy to listen for DOM node elements being added/removed and/or modified/changed. Maybe an idea for a future article? :) Thanks again!

    • Taylor Schaerer

      My mistake. On further investigation it seems I am talking about two different things. In which case I believe this article is still totally valid. Apologies if I cause anyone confusion.

  11. Kumail

    can we detect some how the change in the css properties of the dom elements? could that be added in MutationObserver API?

    • Kumail

      some how to detect change in height/width of the element, whether its due to css change or something due to implicit reason while adjusting another new element being added.

  12. pranay

    Can I observe this.shadowRoot with mututionObserver?

  13. Michael

    What I read helps me to find out *that* something has changed, but in order to find out *when* if has changed, I’d like to see the trace-stack that led to this mutation. Unfortunately when I print “new Error().stack” in my mutattion-observer, it always shows the same stack. Is there some way (perhaps through a different route) to get this info?

  14. Michael

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