DEV Community

Zenovay
Zenovay

Posted on

The tracking script that never blocks page load: a stub and queue pattern

How we built an analytics client that captures everything without ever blocking paint.

The Constraint

An analytics script that slows down the page is sabotage, especially on a marketing site where the conversion you are measuring is the thing you just hurt. So our hard rule: the client must never block page load, and it must never lose an event that happened before it finished loading. Those two goals fight each other, and the resolution is a stub and queue.

The Stub

A tiny synchronous stub defines the global immediately and queues every call into an array. This is all that runs before the real script arrives.

window.zv = window.zv || function () {
  (zv.q = zv.q || []).push(arguments);
};
Enter fullscreen mode Exit fullscreen mode

Now the page can call zv('event', {...}) from the very first line, and nothing is lost even though the real implementation is not loaded yet. Every call lands in zv.q.

Loading the Real Script Without Blocking

The real script loads async, so it never blocks parsing or paint.

<script>
  window.zv = window.zv || function(){ (zv.q = zv.q || []).push(arguments) };
</script>
<script async src="https://cdn.example.com/zv.js"></script>
Enter fullscreen mode Exit fullscreen mode

When zv.js arrives, it drains the queue in order before handling anything new:

const queued = (window.zv && window.zv.q) || [];
window.zv = realImplementation;
for (const args of queued) {
  realImplementation.apply(null, args);
}
Enter fullscreen mode Exit fullscreen mode

The stub and the real implementation share one entry point, so the page never knows or cares which one it is talking to.

Push the Work to the Edge

The browser stays thin on purpose. The heavy work runs on Cloudflare Workers:

  • per site config (which features are on) is cached in Workers KV and read at the edge, so the script does not round trip to an origin to configure itself
  • events batch in the browser and flush with sendBeacon, which survives the page being closed
let buffer = [];

function enqueue(event) {
  buffer.push(event);
  if (buffer.length >= 10) flush();
}

function flush() {
  if (!buffer.length) return;
  navigator.sendBeacon('/ingest', JSON.stringify(buffer));
  buffer = [];
}

addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') flush();
});
Enter fullscreen mode Exit fullscreen mode

The Byte Budget

The whole client sits around 3.1kb gzipped, treated as a hard ceiling. Every feature either fits or loads on demand. Session replay, for instance, is a separate module loaded only when enabled, so customers who never use it never download it.

What We Learned

  • The stub and queue is the entire trick for never blocking and never losing events. It is a few lines and it solves both goals at once.
  • sendBeacon on visibilitychange is what makes the last events survive navigation. A normal fetch on unload gets cancelled.
  • A hard byte budget forces good decisions. Without it, every feature quietly adds weight to the page you are paid to protect.

Honest Limit

Stub and queue slightly delays the first few events until the real script drains the queue. For analytics that is invisible. For anything user facing, it would not be acceptable.


If you ship a third party script, how do you keep it from becoming the slowest thing on your customers' pages?

I build Zenovay with my co-founder, cookieless website analytics on Cloudflare Workers. This is how our client script stays out of the way.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev

The stub-and-queue pattern is a good example of analytics respecting the product it measures. The tricky part is usually not capturing the first events; it is making replay idempotent so queued calls do not duplicate or reorder meaningfully. Measurement should never become the reason conversion got worse.