Shave 100ms off the Plausible Analytics POST response, making it as fast as Cloudflare Zaraz GA4
I moved over to Plausible Analytics; I had been using JetPack Stats. But I’ve never been happy with the (1) extra 3rd party DNS lookup, (2) large JavaScript payload, (3) cookies, and (4) sending data to a 3rd party.
Cloudflare has an excellent solution for Google Analytics GA4 by proxying requests through Zaraz. I tried it. It is fast and easy to implement. But GA4 still sets cookies and sends data to a 3rd party, which didn’t quite meet my goals.
I figure I could use Zaraz with self-hosted Plausible Analytics. Plausible is a web analytics tool with privacy in mind. I never complied with the EU cookie consent banner anyway (I’m not in the EU), but I wish sites all would all go cookie-free. Not because I don’t like cookies. 🍪 I do. But so they can get rid of that cookie consnet banner!
So, Plausible doesn’t support Cloudflare Zaraz. But there is a workaround to proxy Plausible through Cloudflare using Cloudflare Workers from the Plausible documentation. This saves an extra DNS lookup. But the problem is it’s still waiting on the origin server. I tested ~140ms from Eastern US.
![](https://b3n.org/wp-content/uploads/2023/03/plausible-slow-post-1024x786.png)
Now, it’s not “render blocking” so this isn’t horrible, but it’s the slowest item on my site. Visitors from US West get it fast, but it’s 100-200ms from US East and probably around 500-600ms from outside North America.
But, we can make the Plausible Worker as fast as the Zaraz GA4 solution.
The problem is the communication goes like this:
async function postData(event) {
const request = new Request(event.request);
request.headers.delete('cookie');
return await fetch("https://plausible.io/api/event", request);
}
- 👩💻 Client → CF Worker: Here’s the POST data. Waiting for a response.
- ☁️ CF Worker → Origin: Here’s the POST data. Waiting for a response.
- 💾 Origin → CF Worker: “202: ok”
- ☁️ CF Worker → Client: “202: ok” (143ms response time)
But we only care about getting the POST data; the response doesn’t matter. We can change the worker to send a 202 before it knows the response from the origin server.
I thought I’d rewrite the function ask ChatGPT to do it for me.
![](https://b3n.org/wp-content/uploads/2023/03/image-14-1024x203.png)
(gave it original worker script)
![](https://b3n.org/wp-content/uploads/2023/03/image-15-1024x863.png)
ChatGPT made one trivial mistake; the return status should be 202.
async function postData(event) {
const request = new Request(event.request);
request.headers.delete('cookie');
const response = new Response('OK', { status: 202 });
event.waitUntil(async function () {
await fetch("https://plausible.io/api/event", request);
}());
return response;
}
- 👩💻 Client → CF Worker: Here’s the POST data. Waiting for a response.
- ☁️ CF Worker → Client: “202: ok” (9ms response time)
- ☁️ CF Worker → Origin: Here’s the POST data. Waiting for a response.
- 💾 Origin → CF Worker: “202: ok”
![](https://b3n.org/wp-content/uploads/2023/03/plausible-cloudflare-worker-1024x730.png)
Essentially, a Cloudflare Edge location can respond in 9ms instead of waiting 143ms to go all the way to Sandpoint and hit the origin server.
Here’s the complete modified Cloudflare Worker Script:
const ScriptName = '/js/script.js';
const Endpoint = '/api/event';
const ScriptWithoutExtension = ScriptName.replace('.js', '')
addEventListener('fetch', event => {
event.passThroughOnException();
event.respondWith(handleRequest(event));
})
async function handleRequest(event) {
const pathname = new URL(event.request.url).pathname
const [baseUri, ...extensions] = pathname.split('.')
if (baseUri.endsWith(ScriptWithoutExtension)) {
return getScript(event, extensions)
} else if (pathname.endsWith(Endpoint)) {
return postData(event)
}
return new Response(null, { status: 404 })
}
async function getScript(event, extensions) {
let response = await caches.default.match(event.request);
if (!response) {
response = await fetch("https://plausible.io/js/plausible." + extensions.join("."));
event.waitUntil(caches.default.put(event.request, response.clone()));
}
return response;
}
async function postData(event) {
const request = new Request(event.request);
request.headers.delete('cookie');
const response = new Response('OK', { status: 202 });
event.waitUntil(async function () {
await fetch("https://plausible.io/api/event", request);
}());
return response;
}
I can’t think of any downside to this. If the POST is not successful, the client isn’t going to care anyway.
A millisecond is worth a fortune. — Eric Kirzner
Thanks a lot for this guide. I try to achieve the same for my self hosted plausible instance. However, I always get 404 not found if I test the worker script. I tried exactly 1:1 the script you posted, which should work since it uses the official plausible url ? I also tried the code on the plausible docs…. any idea why that’s the case?
Hi, Ars. I just looked at your site, I didn’t get a 404, but you may have turned it off since you posted. Does the official worker script work fine for you?
On Cloudflare, did you set up a worker route for your domain? Under worker routes –> HTTP routes –> route: I set mine to *b3n.org/p/* since I’m calling: const ScriptName = ‘/p/eye.js’;
Thanks a lot! This is a great way to get Zaraz-like performance with Plausible!