Getting Speculation Rules, Prerendering, and Website Analytics to Play Nicely Together

With a few lines of JavaScript, you can improve your website’s performance and still gather useful analytics data.

Google’s Speculation Rules API promises to improve user experience — for Chrome users, at least — by making it possible to prerender pages in the background based on user activity, resulting in near-instantaneous navigation between pages. All it takes is adding some JSON to your website:

<script type="speculationrules">
{
	"prerender": [{
		"where": {
			"href_matches": "/*"
		},
		"eagerness": "moderate"
	}]
}
</script>

With that code in place, Chrome begins “speculating” about future user activity by prerendering pages when a user holds their pointer over a link for 200 milliseconds or presses down on their mouse button or trackpad. If and when the user actually clicks the link, Chrome replaces the current page with the prerendered page almost instantly. In action, it’s pretty impressive and definitely makes a website feel faster and more responsive.

There are, however, some potential downsides to this speculation. As Google’s documentation notes, “prerendering does use additional memory and network bandwidth.” Chrome therefore implements some limits on speculation, including the number of pages that it’ll prerender and store in memory at any given point in time. But another, more subtle downside of prerendering is that it can negatively skew website analytics.

Most website analytics tools, like Google Analytics, work via a small snippet of JavaScript tracking code that’s added to every page on a website. When a page loads, this tracking code is also loaded, notifying the analytics tool that the page has been visited and to start collecting data about the visit.

This process can also occur, though, when a page is prerendered. Which is fine so long as the user clicks the link. But remember, this is all speculation. Chrome prerenders the page because it seems likely the user will click the link. If they don’t, then you’re left with a false positive. That is, the tracking code fires, and the analytics tool records a visit, because the page was prerendered, not because it was actually visited by a user. This might not happen very often, but on a large enough website and over a long enough period of time, it could lead to a non-trivial skewing of the website’s statistics.

One possible solution is to prefetch pages rather than prerender them, which can be done by slightly altering the above JSON:

<script type="speculationrules">
{
	"prefetch": [{
		"where": {
			"href_matches": "/*"
		},
		"eagerness": "moderate"
	}]
}
</script>

Prefetching is triggered by the same user activity that triggers prerendering. But with prefetching, Chrome just grabs a page’s resources (e.g., images, CSS files) and caches them. If the user clicks the link, Chrome still needs to download and render the page, but those resources are already available and don’t need to be downloaded. This can still result in improved performance, though it might not be as impressive or noticeable as with prerendering.

Fortunately, there’s another option if you want to use prerendering but compensate for any potential analytics skewing: Dynamically load tracking code when it’s the right time to do so. Add this JavaScript to every page on a website:

<script>

const whenActivated = new Promise((resolve) => {
	if (document.prerendering) {
		document.addEventListener('prerenderingchange', resolve, {
			once: true
		});
	} else {
		resolve();
	}
});

async function addScript(scriptUrl) {
	await whenActivated;
	const script = document.createElement('script');
	script.src = scriptUrl;
	document.body.appendChild(script);
}

addScript('https://analytics.tool/foo.js');

</script>

When it loads, it creates a constant called whenActivated. This holds the value of a JavaScript “promise” that checks if the page was prerendered or not. If the page was prerendered, the promise won’t resolve until the page is activated (i.e., the user clicks the link and views the page), which fires the prerenderingchange event. Otherwise, if the page was visited normally and not prerendered, then the promise resolves immediately.

The addScript function is then defined, which — upon successful resolution of the whenActivated promise — accepts the URL of an external script (the tracking code, in this case), creates and formats the necessary <script> tag, and appends it to the <body> element. (Note: addScript is an async function, meaning that the <script> tag will effectively be loaded asynchronously.)

Finally, the addScript function is fired, with the tracking code’s URL passed in via the scriptUrl parameter.

You might need to adjust the above JavaScript depending on your analytics tool’s unique requirements (e.g., adding the script to the <head> instead of the <body>). Google’s documentation offers several other approaches for dynamically updating prerendered pages to reduce any impact on analytics.

I added the above JavaScript to Opus earlier this month and to date, haven’t noticed any negative impact on gathering website traffic data via Tinylytics (my preferred analytics tool). More importantly, though, speculation rules and prerendering mean that my Chrome-using readers are now able to enjoy a better user experience whilst perusing my site. (Here’s hoping that Firefox and Safari also add speculation rules support in the near future.)

Enjoy reading Opus? Want to support my writing? Become a subscriber for just $5/month or $50/year.
Subscribe Today
Return to the Opus homepage