← All posts

Zero-JS was not enough: edge-caching SSR HTML on Cloudflare Workers

How the author stabilized Core Web Vitals after dropping React: Cache API, cookie-gated HTML, removing Zod from the browser, and refactoring Astro forms.

Zero-JS was not enough: edge-caching SSR HTML on Cloudflare Workers
Contents

In brief

The author removed React and hydration, reached Total Blocking Time = 0 ms, yet PageSpeed still swung between 90 and 100. The culprit was server-side: every request rendered fresh HTML on Cloudflare Workers. The fix — middleware using the Cache API, personalization gated on two cookies, plus a series of small wins around forms and CSP.

What happened

The story opens with a confession: a zero-JS build eliminated main-thread blocking, but lab PageSpeed runs still jittered. Four measurements in ten minutes returned 96, 100, 98, 96 — with no deploy. The variable was not in the browser but in HTML generation time at the edge.

GTmetrix showed a root document up to 1.2 s with no framework at all — surprisingly painful for a site that looks static. The author logged seven findings in a dedicated document (diagnosis separate from fixes) and closed them by priority: quick wins first, architecture last.

Among the findings: a 29.8 s Google Ads Audiences beacon via Zaraz (disabled with one checkbox), Zod v4 using new Function() in the client bundle (breaking CSP without 'unsafe-eval'), a hidden subscribe step that still shipped JS on first paint, and a select forcing reflow on every resize.

The main architectural move — htmlCacheMiddleware: anonymous GET requests without personalization cookies are cached in caches.default for 300 s; responses with newsletter/consent cookies always bypass cache. Key = origin + pathname (locale in path, query stripped). After shipping, root document with X-Edge-Cache: HIT landed around ~140 ms instead of 1.2 s.

Why it matters

Many teams optimize only the client: drop JS, split bundles, tune CLS. But with SSR on Workers, TTFB and render variance hit LCP and lab score stability directly. A Cache-Control header on HTML is often useless when run_worker_first is on: the Worker answers before CDN cache layers, so the only HTML cache is the one the Worker checks itself.

The article is a useful edge-native case study: how not to poison cache with Set-Cookie (cookie-less request → cookie-less response), how to split edge TTL (s-maxage only on stored copies) from browser policy (private, no-cache on all HTML responses), and how to replace heavy client validation with a ~20-line mirror while keeping server Zod as source of truth.

In practice

  1. Measure variance — several PageSpeed/GTmetrix runs back-to-back; if TBT = 0 but Performance swings, inspect TTFB and root document time.
  2. Cache anonymous HTML in the Workercache.match before the middleware chain; personalize only via an explicit cookie list.
  3. Never mix Set-Cookie and edge cache — sync lang cookie only when it already exists and disagrees with the URL.
  4. Audit third parties — audience sync can hold Fully Loaded for tens of seconds with zero product value.
  5. Zod on server, light mirror in browser — one safeParse interface, same error codes; server stays authoritative.
  6. Lazy-load hidden stepsdynamic import the segmentation controller only after a successful subscribe.
Finding Effect after fix
No edge HTML cache Root ~140 ms (HIT)
GA Audiences beacon Fully Loaded down from 31.6 s
Zod in client Best Practices 100, no eval in CSP
Newsletter domain refactor Less JS on first screen

Takeaway

Zero-JS is necessary but not sufficient for stable Core Web Vitals on SSR at the edge. Cookie-gated Cache API, strict CSP, and form discipline brought medians around ~100 mobile / 98 desktop — with the understanding that lab noise never fully disappears. If your site runs on Workers with i18n and personalization, walk through the article’s checklist before the next “magic” framework deploy.

Comments

Loading comments…