Measuring a website’s performance using MutationObserver

Ramiro Medina
4 min readMar 14, 2022

--

You click a button on a website, then something changes. How long did it take? It’s certainly a basic question. But it’s not straightforward to answer.

Photo by Manik Roy on Unsplash

A couple of weeks ago, I wanted to measure how my proof-of-concept framework Streamsync stacked against market leader Streamlit. More specifically, I wanted to know what was the end-to-end time between an event (such as a click) and a result (change in the DOM).

The most basic case I tested is shown below. I wanted to know how much time it passed from the moment I clicked the button “Increment” until the text node with the content “The count is 2” changed to “The count is 3”.

The challenge is easy to understand, but coming up with a reliable solution isn’t trivial.

Popular techniques

Modifying the source code and adding calls to performance.now()

This alternative is based on the method performance.now(), which is essentially a high resolution timer implemented with performance measurement in mind. In principle, using it is as simple as:

  • Calling performance.now()
  • Doing something
  • Calling performance.now() again
  • Calculating the difference between the two calls to performance.now()

Thus, creating something that resembles a sandwich, where performance.now() is the bread.

const startTime = performance.now();
results.forEach(result => { show(result); });
const endTime = performance.now();
console.log(`It took ${ endTime - startTime } milliseconds.`);

This works, but there are a few caveats:

  • It requires us to become familiar with the code we’re trying to measure performance of.
  • We need to actually modify the code. Although this can be done via Chrome DevTools, it’s time consuming. And if the code is minified or obfuscated, it’s painful.
  • It’s harder to implement if the code we’re trying to measure performance of isn’t neatly sitting in a single synchronous block.

Using Chrome DevTools’ Performance tab

Another alternative is to use Chrome DevTools to record the session, then analysing it to understand how time passed. Even if async calls are made, we’ll be able to spot them in the timeline.

Like the alternative presented above, it works. However, it also comes with a few caveats:

  • Session recordings generate a lot of data. Finding exactly what we’re looking for can take some time.
  • Getting multiple measurements can be really boring and take a long time. It’s unlikely we’ll want stop at one observation, as our measurement would be completely unreliable. Given the point above and the fact that we have to manually identify each iteration, we can expect to spend a lot of time staring at that timeline.
  • There’s a manual element to it, making it challenging to get reliable measurements.

Proposed technique: MutationObserver and EventListener injection via Chrome DevTools

This approach uses performance.now(), but it’s independent from existing scripts and relies only on the rendered DOM.

To get started, identify the DOM element that will be triggering the event. In this case, it’s the “Increment” button. Find it on the Elements tab, right click on it and select “Store as global variable”. This will store a reference to the DOM element in the variable temp1, as shown below.

Following the same logic, store a reference, in temp2, to the element that we’ll be watching for changes. In this case, it’s the div with the text “The count is 0”.

Run the following code in the console.

var performStart;
temp1.addEventListener("click", () => { performStart = performance.now(); })
callback = () => {
const elapsed = performance.now() - performStart;
console.log(`${ elapsed } milliseconds.`);
};
observer = new MutationObserver(callback);
observer.observe(temp2, { attributes: true, childList: true, subtree: true, characterData: true });

This will attach an EventListener to temp1 and listen for click events. Once a click happens, the timer value will be stored in the global variable performStart. Feel free to tweak the code to listen to other event types, such as mouseover.

The code will also create a MutationObserver that will trigger the function in callback when the DOM element in temp2 is modified. The callback will calculate and display the time elapsed between the firing of the event and the DOM mutation.

We now have reliable, easily repeatable measurements without modifying the source code.

--

--