Observing element changes with MutationObserver in JavaScript

The concept

MutationObserver informs you whenever a change (mutation) on a element or its child elements occurs. A change can be a modification of an element’s attributes, its text or if there was a child node appended or removed.

A very powerful mechanism! But it comes at a cost in terms of performance. That’s why this API offers many detailed configuration options and methods that you should use carefully.

Observation is expensive. Be as specific as you can be when defining what to observe.

Just me

Example: Observing an element and reacting to changes in two steps

In this example we want to observe the following element for changes:

<p id="observed">I am observed</p>

Step 1: Create MutationObserver instance and callback

The callback answers two questions: “What has changed?” and “How do I want to react to these changes?”

// defining what to do when an observed change happened
let callback = (mutationsList, observer) => {
    for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('A child node has been added or removed.');
        } else if (mutation.type === 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

Now we create an instance of MutationObserver and pass in the callback.

let observer = new MutationObserver(callback);

Step 2: Call observe on the MutationObserver

Calling observe will answer two questions: “Which element do I want to observe?” and “What in particular do I want to observe on that element?”. More on that later.

let observedElement = document.getElementById("observed");

// configure what to observe on the element
let config = {
    attributes : true,
    childList : true,
    subtree : true
};

observer.observe(observedElement, config);

Now let’s trigger some changes:

// changing style will trigger mutation of type 'attributes'
observedElement.style.color = "red";

// appending a child will trigger mutation of types 'childList'
let child = document.createElement('span');
observedElement.appendChild(child);

// appending a child to the child will trigger mutation of types 'childList' again
child.appendChild(document.createElement('div'));

Important: The MutationObserver callback is invoked only after the end of the script execution, that is: Not after line 2, not after line 6 but after line 10.

Defining what changes to watch for

Watching for changes can be expensive (in terms of performance, CPU and RAM resources). That’s why it is a good idea to define exactly what changes you want to observe. That’s what the second parameter of observe is for.

Side note: Mozilla’s doc is specifying this object as optional, but in my tests you get an exception if you do not specify it:

Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true.

Observing specific attributes

We use attributeFilter to specify that we only want to observe the attribute class for changes. Of course any mutation to style will not trigger our MutationObserver callback.

// only observe 'class'-attributes
let config = {
    attributeFilter : ['class'],
    attributes : true,
};

// we observe now, but callback is only triggered after script is done executing
observer.observe(observedElement, config);

// no mutation is triggered, because we do not observe 'style' this time
observedElement.style.color = "red";

Observing text changes

Yes, we can observe changes of an element’s text node. In the following example we change the text from “I am observed” to “I was changed”. Observing changes of text nodes can be done using characterData property on the config object. We even set an option that allows us to record the old text value by setting characterDataOldValue to true.

let observedElement = document.getElementById("observed");

// defining what to do when an observed change happened
let callback = (mutationsList, observer) => {
    for (let mutation of mutationsList) {
        if (mutation.type === 'characterData') {
            console.log(`Character was changed from '${mutation.oldValue}' to '${mutation.target.data}'`);
        }
    }
};

let observer = new MutationObserver(callback);

// we configure to observe text changes
let config = {
    characterData: true,
    characterDataOldValue : true,
    subtree: true
};

observer.observe(observedElement, config);

observedElement.firstChild.textContent = "I was changed";

// Side note: The following would not trigger a characterData mutation, but a ChildList event
observedElement.innerHTML = "I was changed by adding a new text node";

Multiple observations on a single MutationObserver

No problem, you can call observe() multiple times on the same MutationObserver. You might want to do that to watch for changes of different parts of the DOM tree and/or different types of changes. Remember, both can be specified as parameter of the observe method, whereas the MutationObserver instance merely processes what to do when a change happened.

But here is a catch: If you call observe() with a node that’s already being observed by the same MutationObserver, all existing observers are automatically removed from all targets being observed before the new observer is activated. On the contrary, if the same MutationObserver is not already in use on the target, then the existing observers are left alone and the new one is added.

[ciu_embed feature=”mutationobserver” periods=”-1,current,+1,+2″]

[forminator_quiz id=”2359″]

About Author

Mathias Bothe To my job profile

I am Mathias from Heidelberg, Germany. I am a passionate IT freelancer with 15+ years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I create Bosycom and initiated several software projects.

No comments yet.

Leave a comment