In this article I discuss multiple use cases for MutationObserver in CRO and why it is a great tool for anyone creating A/B tests. If you would like to read in-depth about MutationObserver and how it works, I would highly recommend this article by Louis Lazaris for Smashing Magazine. Alternatively you can read the MDN Web Docs for MutationObserver.
Introduction
I currently work in the CRO (Conversion Rate Optimisation) department as a Front-End developer for a global marketing business. Every week I build multiple A/B tests for a variety of clients. Developing A/B tests usually comprises of creating some JavaScript and accompanying CSS and implementing it to a percentage of a website’s traffic, using optimisation tools such as VWO, Google Optimize and more.
Due to the code I create being injected by these tools, this means that I don’t have access to any of the existing Back-End processes. I write my code to co-operate with whatever is available on page load. This causes issues when trying to apply changes to dynamic content, such as lazy loading product listing pages for example. It is in situations just like this where the mighty MutationObserver API comes into play!
MutationObserver for CRO 101
Simply put, MutationObserver provides the ability to watch (or observe *winks*) the DOM for any changes that may occur. There are 3 basic things that a MutationObserver needs to get going, these are:
- Callback function (what you want to happen when a mutation happens)
- Target element (which element do you want to observe/watch)
- Config (which mutations do you want to observe/watch)
Here is the basic syntax that I always start my mutation observers with:
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Watch for childList and subtree mutations
const observerConfig = {childList: true, subtree: true};
// Choose the target (what to observe)
const target = document.querySelector('#example_element');
// Start observing the target node, using the config created above
observer.observe(target, observerConfig);
The most common mutations that I use when creating A/B tests are the “childList” and the “subtree”, which are both used in the example syntax above. The “childList” option will observe the target node for any child elements being added or removed. The “subtree” element will observe the target node and all of it’s descendants.
Example - 10% off everything
A popular format of an A/B test is to add something to product listings within a product listing page. For example, adding a warranty icon to every tech item within a product list. With many e-commerce website’s product listing pages implementing lazy load or “load more” functionality, listings are dynamically added to the page as the user scrolls down the page. This means that adding the A/B test content to the page on page load is not a viable option, as the changes would only get applied to the initially loaded set of products.
In the example below, I have implemented a MutationObserver to the product listing container. The callback function within this MutationObserver will get the price of the product, calculate a new price with 10% off and then insert it (new price in red). For this example I have added a 1.5 second delay before implementing the new price, this is to make the changes that are being made more obvious.
Below is the code used for the above example, working on the nike.com/gb website as of 7th January 2020.
let observer = new MutationObserver(discountPrice);
let observerConfig = {
childList: true
};
let target = document.querySelector(".product-grid__items");
observer.observe(target, observerConfig);
discountPrice()
function discountPrice() {
const all_products = document.querySelectorAll(".product-card:not(.checked)");
all_products.forEach((product, i) => {
// Get Current Price
let price_elem = product.querySelector(".product-price.is--current-price")
let price = Number(price_elem.textContent.replace("£", ""));
// Calculate 10% off
let discounted_price = price * 0.9;
setTimeout(() => {
price_elem.insertAdjacentHTML("beforebegin",
`
<div class="product-price 10-off" style="margin-right: 15px; color:#FF0000;">
£${discounted_price.toFixed(2)}
</div>
`
)
console.log(`Mutation (${i})`)
}, 1500)
product.classList.add("checked");
})
}
Example - Pokemon Weight Converter
Another popular use case for a MutationObserver is content that gets replaced. Examples of this may be a product details page where a user may change a selection and the price changes, or a checkout page where a user applies a discount code and the price changes. Sure you could add a “change” or a “click” event listener and then use a “setInterval” to check every 250 milliseconds for a change in price, or you could use a MutationObserver.
Below is another example that I created, I am fetching data on different Pokemon using the Pokeapi and then creating a small detail card. The only issue is, the weight that comes through from the API is in hectograms. I have implemented a MutationObserver that checks for a change in the weight, then creates a new element to show that weight converted into kilograms.
See the Pen Mutation Observer Basic Details Page Example by Jonny (@JonnyMarsUK) on CodePen.
You can see the code for the above example by interacting with the Codepen widget!
Tips & Things to be aware of
- If the item you are observing gets removed or replaced, the mutation observer will no longer be effective.
- If you are making changes within the target element and are using the “childList” config option, or are making changes within a descendant of the target element and are using the “subtree” config option, you will trigger an infinite loop which will crash the page. If you need to make changes within the target element, you will have to use the disconnect method to disconnect the MutationObserver whilst you make the changes (at the start of your callback function) and then reconnect the MutationObserver once done.
- Adding a class to an element that has been checked, and then checking for that class on later mutations helps prevent duplicated content. You can see an example of this within the code for Example 1 above.