Using MutationObserver to stop a flaky plugin wrecking your beautiful site

Ji Pattison-Smith
JavaScript in Plain English
4 min readOct 8, 2020

--

On the lookout for mutations in Lake DOM

Don’t you love it when clients ask you to “just stick this script on the site”? There’s rarely any “just” about it as these things always have knock-on effects, and this happened to me recently when a client installed a live-chat plugin on their website. It looked terrible, and there wasn’t much I could do about that, but it also overlapped some crucial features on mobile, rendering them unusable.

I could get round this by adding a class to the body and updating various styles accordingly, but that only works if the live chat is in place, which wasn’t guaranteed.

The plugin was sometimes enabled, and sometimes disabled. Sometimes it would load, sometimes it wouldn’t. When it did load, it took 15–20 seconds. And worst of all, there was no “hey, I loaded successfully” event emitted. So I needed some way to tell if the live chat monstrosity was there or not.

I started looking at some kind of polling script, checking the DOM at regular intervals to see if it had loaded yet (and I hated myself for even considering it), but then a lightbulb went off in my head — don’t we have a fancy API for that sorta thing now?

As it turns out, yes we do! It’s called MutationObserver and it’s been supported in Chrome and Firefox since 2012, and even works in IE11. It’s pretty nifty. You tell the browser to watch for some specified change in the DOM, and it lets you know when it spots that change. It’s like when you’re trying to get in touch with customer service, and you can press 1 to get a callback when it’s your turn instead of sitting on hold (aside: why don’t all companies do that?!).

There are three main parts to an observation:

  1. Target element — which part of the DOM will you watch?
  2. Configuration — what kind of changes are you looking for?
  3. Callback — what will you do when the change happens?

The live chat window gets appended to the body, so my target element had to be the body. This sounds expensive (computationally-speaking), but in the config we’ll make sure we’re not watching the whole DOM.

const targetElement = document.body;

What we’re looking for is specifically new child elements. There’s an option for that, called childList. The other main options are:

  • attributes, which watches for changes to attributes on the element itself
  • subtree, which watches the whole DOM tree from your target element down

You can mix and match these, but you only need to specify the options you want to enable since by default the others are false. So our config is pretty simple:

const config = { childList: true }

Finally, the callback, which is the trickier bit. We get two objects passed — a list of mutations and the actual observer.

We’ll loop through the mutations, and for those which are of type childList we’ll see if there are any added nodes. You can also look at nodes that were removed from the target, and the target itself. For each node, you get access to the full element with all its properties — just the same as if you’d used getElementById or querySelector. Once we find the node we’re looking for, we can add the class to the body and disconnect the observer, so we’re not using up precious CPU cycles.

This is what my callback looks like:

const callback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const addedNode of mutation.addedNodes) {
if (addedNode.id === 'live-chat-window') {
document.documentElement.classList.add('live-chat-loaded');
observer.disconnect();
}
}
}
}
}

Now all that remains is to fire up the observer:

const observer = new MutationObserver(callback);
observer.observe(target, config);

A tiny amount of code, and not a setTimeout in sight!

That’s the simple example. Now for a some improvements we could make.

Robustness: To prevent race conditions, we should check if the node exists before setting up any observers.

Performance: Looping through mutations might not be the best for performance. Doing getElementById could be much quicker (although in my actual problem I didn’t have a predictable ID to check).

Compatibility: There are browsers out there which don’t support MutationObserver, so it’s good practice for to check for its existence before trying to use it (if (window.MutationObserver)). Polyfills exist, but I’ve no idea if they’re any good. You’ll also need to change the for…of loop to a simple for if you want IE support (or let your transpiler do it for you).

If you want to read more about MutationObserver, check out the MDN page and the Smashing Magazine guide.

Enjoyed this article? If so, get more similar content by subscribing to Decoded, our YouTube channel!

--

--