Observing DOM through MutationObserver API

Malhar Shah
WhatfixEngineeringBlog
6 min readDec 23, 2020

We witness (observe) many events (changes) happening everyday around us. These events include a building constructed on empty ground or a bench in a park painted green and so on. And how do we observe these changes? The simplest answer can be ‘We see it through our eyes, of course’. Yes, you’re right! (and remember this example, I got a fun question for you at the end).

Now, let’s consider the similar scenario when it comes to web application(s). Do we see any changes happening on the page when we interact with it? Like clicking on a button, opens up a form or color of button/link changing when we hover over it. These changes are observed by us through our eyes. But, how can we programmatically identify these changes and let the program know that it needs to perform these actions (open form/change colour) based on the user’s activity. There are many ways of handling it and one such way is to use the MutationObserver(MO) API. I’ll walk you around all nitty-gritties of MO API by covering following points:

  • What is MutationObserver?
  • How to use it?
  • Points to keep in mind while implementing/using this api.

So, without further ado, let’s get started.

MutationObserver API

What is MutationObserver?

If a non-tech person asks me this question, I would simply say it is a means by which you can observe different changes happening on your web page. Same answer for a tech person would look something like — ‘It is an interface by which you can observe mutations happening on your DOM’. MutationObserver API was developed as a replacement to Mutation Events.

A sample code snippet or blueprint for implementing MutationObserver API is added below. It is an adaptation from this MDN article.

One of the questions which comes to mind for most is ‘Can I rename targetNode/config/callback/observer to something else or do we have to use these names as keywords itself?’. Answer is, yes you can. Just make sure you give relevant/meaningful names so that others reading your code can better understand your code.

Also, don’t worry if you don’t understand the above code right now. I’ll explain everything below in ‘How to use it?’ section.

Note: For all the explanations below, I’ll refer to the above code as blueprint.

How to use / implement this API?

If you look closely at the blueprint, there are six points to keep in mind when we use MutationObserver. Let’s get started from the first one.

1. Selecting a target node to attach observer to:

First thing to do is decide where exactly you want to attach your ‘observer’. Let’s call this node as targetNode. This is important, because your observer will observe all mutations on this targetNode and (possibly) targetNode’s subtree in DOM based on your configuration. However, it won’t observe any changes on any of the other nodes. In the blueprint, I have added it to the document of the page.

2. Selecting what type of changes we want observer to observe:

Second thing to do is decide what type of changes you want to observe. There are many different types of changes which MutationObserver allows you to observe. Below is a list of all.

  • childList: if true, observe changes in the direct children of targetNode
  • attributes: if true, observe changes related to attribute of targetNode
  • characterData: if true, observe changes in the text content of the node like input fields (targetNode.data)
  • subtree: if true, just tells the observer to observe changes in all descendents of the targetNode. It doesn’t say which/what changes to observe.
  • attributeFilter: if set, observe changes only to these specific attributes in the array.
  • attributeOldValue: if true, it gives you both, the old and the new value of attribute to callback, otherwise only the new one. Note: For this to work, the attributes option should be set to true.
  • characterDataOldValue: if true, it gives you both, the old and the new value of node.data to callback, otherwise only the new one. Note: For this to work, the characterData option should be set to true.

It is not necessary to add all these values to the config. These are added just for your reference. You can choose properties based on your requirement. As you can see in the blueprint, I have only used 3.

Note: In order for MutationObserver to work, at least one of ‘childList’, ‘attributes’ or ‘characterData’ needs to be set to true, otherwise an error will be thrown.

3. Creating a callback function to be invoked by observer:

Now, let’s create a callback function. This callback will be invoked whenever any change is observed by the observer.

As you can see, callback takes two arguments.

  1. List of MutationRecord objects
  2. Observer itself

Question arises, what is a MutationRecord? Simply put, a single MutationRecord object is a single change observed by the observer. Below are its properties:

Sample MutationRecord object
  • type: Current mutation type. It can be one of: attributes: attribute modified / characterData: data modified, used for text nodes / childList: child elements added or removed
  • target: On which node the change occurred. It can be an element for attributes or text node for characterData or an element for a childList mutation.
  • addedNodes/removedNodes: List of nodes that were added/removed.
  • previousSibling/nextSibling: List of previous and next sibling to added/removed nodes.
  • attributeName/attributeNamespace: The name/namespace (for XML) of the changed attribute.
  • oldValue: The previous value of the attribute/text. Only for attribute or text changes, if the attributeOldValue/characterDataOldValue is set to true.

4. Initializing MutationObserver:

Initialize MutationObserver using the MutationObserver constructor. It takes the callback function as the argument.

5. Let’s start observing these changes:

Now, we are set to start observing the changes. As soon as you call the observe method, your observer will start observing. As arguments, you need to pass targetNode and the configuration for the observer.

6. Disconnect the observer:

When the task is done and you don’t need to observe any more changes, call disconnect method to stop observing.

Points to consider while implementing/using this API:

MutationObserver API is generally used on big and complex pages and it might impact the page loading/rendering time. This depends on how much computation is done inside the callback, as callback is executed as a microtask which blocks processing of DOM. Also, another thing to keep in mind is that callback might get fired hundreds or thousands of times for a single user interaction with the page. So, here in this section, I’ll recommend few tips which might help you while implementing MutationObserver:

  • Whenever observing changes of type attributes, it’s better to use attributeFilter, if possible. This would reduce the number of callbacks we receive, which in turn will reduce our computation load.
  • If possible, observe direct parents of a child (subtree: false). For example, wait until the parent element of the concerned child node appears, by attaching an observer on the document of the page. Once found, disconnect this observer and add an observer with subtree: false on this parent node. This would reduce the scope for observer on what all elements to observe for mutations and in turn reduce the number of callbacks.
  • If element occurrence is rare on page, it’s probably better to use getElementsByTagName and/or getElementsByClassName and check this list, than to loop over the list of MutationRecord objects as MutationRecord object list might contain thousands of entries. Similarly, if waiting for an element which has an id, it’s better to use getElementsById as it is much faster than looping over the list of MutationRecord objects.
  • Avoid using querySelector and especially the extremely slow querySelectorAll inside the callback.
  • Use debounce or similar technique, for example, accumulate mutations in an outer array and schedule a run via setTimeout / requestIdleCallback / requestAnimationFrame:

Check out more of such tips here.

Conclusion:

Phew !! Finally done !! I hope the above blog helped you in knowing about MutationObserver API and how to use it efficiently and effectively.

So now here’s my question for you

Earlier in the blog, I gave an example of a building and a bench. Can you create an analogy between that example and MutationObserver api?

Hint: See the configuration options and what each type is used for.

Let me know your answer in the comments below.

Here, at Whatfix, we have used MutationObserver API for some of our features to observe elements at runtime. Below is a glimpse for the same:

Signing off for now. We’ll meet again in the next blog!

References:

--

--