In the early days of the web, non-technical users (and a few technical ones too) would wait for the browser spinner to stop spinning before interacting with a web page, which in terms of JavaScript, was when the window.onload
event fired (with non-obvious exceptions based on the fancy of various browser authors).
This restraint was not unfounded since many pages loaded images without specifying a height or width, so content could jump out from under your mouse if you clicked while something rendered, or you’d have a page that depended on JavaScript that only ran in the onload
event, or even worse, JavaScript that used document.write
to modify the contents of the page.
Shudder!!
Well, thankfully those days are mostly behind us, and there are ways to deal with bad third parties that still use document.write
.
This means that users have gotten used to interacting with pages before the spinner stops spinning, and page developers have become smarter about building pages that are usable before they are fully loaded. So for everyone hoping to measure perceived performance, the window.onload
event is no longer the best event to determine when a page is usable.
In the past, we’ve looked at various approaches of determining in a generic manner when a particular page is usable, but for a given site, the developer or site owner knows the use cases that the site was designed to handle, and knows which elements are pre-requisites for a particular user to act on their intention.
Once we start thinking in terms of user intentions, we are no longer constrained by the concept of a page, so our measurement strategy will work equally well with single-page apps and can also be extended to an entire user journey across multiple pages (or intents).
In order to measure this intent, we need at least two interesting events, one where the user signals their intent to do something, and the second when the page is in a state where they can accomplish their task or their task is complete.
The user may signal intent by entering a URL into the browser, clicking on something, or submitting a form.
An intent to read an article is typically considered complete when the content of that article is visible in the browser, while my intent to submit this article is complete once I hit the Publish button and the browser responds with a success state.
An intent to purchase a gift for someone, on the other hand, may involve a long process with multiple clicks, and submits going through multiple products, reviews, and a checkout process.
Today we’ll look at some simple code to measure when certain things have happened on a page using a MutationObserver
.
If a user clicks on something, we set a timer, but we also start a MutationObserver
:
This MutationObserver
will look for any changes to the DOM starting from the document
object and nesting down into all children and will call the callback
on those changes.
The callback needs to do a few things:
So this is all fairly simple, and it’s well documented. The part we really care about is the “check for interestingness” that I’ve included in comments above.
Unfortunately, the MutationObserver
does not work well when changing innerHTML
. If you manipulate an element’s innerHTML
, only top level elements in the HTML fire the callback, so we do something else:
Multiple page journeys take this idea a little further. The technical matters of timing the events remains the same. The big difference is that you now need to leverage localStorage
or cookies
to store timer information while you wait for the journey to complete.
There are a large number of documents on working with both, so I won’t go into the code today.
According to CanIUse, MutationObserver is fairly well supported among modern browsers, and there’s a polyfill for older browsers.
We have been thinking about these approaches for a while now. Buddy and I presented about it at Velocity EU this year, and Nic Jansma and I have been working on getting the code into boomerang.
I’d especially like to thank Nic for all his work on this project.