Real-World Uses for MutationObserver

MutationObserver is a lesser known JavaScript feature which allows you to detect when elements in a web page are inserted, changed or removed. It is still relatively new, but it is supported by every modern browser.

The web is full of demos and tutorials of MutationObserver, but it’s pretty hard to find examples of it actually being used in practice. Even a search of Github is almost all libraries and test cases. We’ve had a couple occasions to use it at Eager however, which I now have the opportunity to share.

Client-side Image Optimization

Believe it or not, it’s actually possible to swap the src’s of img tags before the browser begins to load them. We can use that to optimize our images without changing the HTML source of our page. This code uses a FireSize service to handle the actual optimization.

We start by setting up a MutationObserver which will call our checkNode function with any new nodes which are added to the DOM:

Code language: JavaScript

var observer = new MutationObserver(function(mutations){
  for (var i=0; i < mutations.length; i++){
    for (var j=0; j < mutations[i].addedNodes.length; j++){
      checkNode(mutations[i].addedNodes[j]);
    }
  }
});
 
observer.observe(document.documentElement, {
  childList: true,
  subtree: true
});

If we run this code early in the head of the page, it will call our checkNode function with each DOM node as the browser parses the page’s HTML. This gives us the ability to check or mutate these nodes before they’ve ever been rendered.

We can define our checkNode function to decide if this is an image for us to optimize.

Code language: JavaScript

checkNode = function(addedNode) {
  if (addedNode.nodeType === 1 && addedNode.tagName === 'IMG'){
    addedNode.src = optimizeSrc(addedNode.src)
  }
}

Finally, we can define optimizeSrc to switch out our image’s src for an optimized one:

Code language: JavaScript

optimizeSrc = function(src) {
  return "//firesize.com/" + src;
}

For a complete implementation, take a look at our FireSize app source code.

Initializing When An Element Becomes Available on the Page

It’s a common pattern to wait for jQuery.ready or DOMContentLoaded to initialize code which depends on elements on the page. Those events don’t fire until the entire DOM has loaded however, meaning the page will start to be rendered before you have a chance to change or add to its content.

Our pattern from the image optimization solution also works for detecting when any element becomes available, allowing you to initialize code which depends on that element at the exact first moment it’s possible. We can redefine checkNode to instead check if our element matches an arbitrary selector:

Code language: JavaScript

checkNode = function(addedNode) {
  if (addedNode.nodeType === 1){
    if (addedNode.matches('.should-underline')){
      SmartUnderline.init(addedNode);
    }
  }
}