ScribeToAny is a full-stack audio and video transcription platform built on React 19 and deployed to Cloudflare Workers at the edge. Heavy GPU workloads (Whisper transcription, diarization, translation) run asynchronously on Modal, while our web application, authentication, database queries, and SSR are powered by Cloudflare Workers.
While our GPU pipeline was fast and asynchronous, our front-end performance audit delivered a harsh wake-up call: real-world user monitoring (Cloudflare Observatory) showed our 75th-percentile TTFB was 3,128 ms — with over 57% of hits rated "poor". Direct curl tests hitting cold edge nodes clocked TTFB between 3.4s and 3.6s for the homepage and SEO tool pages.
Yet, Cloudflare's internal Worker metrics showed a median CPU wall time of just 5 ms.
How could a Worker that finishes its work in 5 milliseconds take 3.5 seconds to return a response?
Here is the complete engineering breakdown of how we diagnosed the cold-start bottleneck, implemented zero-staleness edge HTML caching, decoupled our client bundles, slashed mobile hydration TBT, and took ScribeToAny to a 95+ Lighthouse performance score.
1. The Paradox: Why 5ms CPU Took 3.5s TTFB
Cloudflare Worker CPU metrics only record active handler execution. They do not account for isolate creation, bundle downloading, and V8 script compilation.
When we inspected our deployment pipeline, two factors collided:
- An 11MB Worker Bundle: Because ScribeToAny includes rich SEO tool routes (over 80 audio/video conversion and transcription tools), markdown renderers, and format converter utilities, the bundled worker script reached ~11MB.
- Low Baseline Traffic Density (~0.1 req/s): With sparse initial traffic, Cloudflare edge PoPs frequently evict idle V8 isolates.
Almost every new visitor — especially organic search traffic arriving from Google — hit a brand-new, cold isolate. Before running our 5ms SSR handler, V8 had to load and compile an 11MB JavaScript payload. The result was a 3-second cold-start penalty on first visit, completely undermining the user experience and SEO ranking potential.
Furthermore, on mobile devices, initial Lighthouse runs flagged heavy Total Blocking Time (TBT): third-party authentication scripts and oversized vendor chunks were monopolizing the main thread during hydration.
We solved this through a systematic four-layer optimization strategy.
2. Layer 1: Edge HTML Caching with Zero-Staleness Build Invalidation
Cloudflare Workers do not automatically cache dynamic SSR responses. Every incoming GET request was hitting our Worker, forcing a cold-start compilation.
Because our marketing pages, blog, and SEO tools are public and identical for anonymous visitors of the same URL, we implemented edge-level HTML caching directly in src/server.ts using the Workers Cache API (caches.default).
Strict Isolation & Bypass Rules
Edge caching dynamic web apps requires strict safety boundaries:
-
Session isolation: If the incoming request has a
better-auth.session_tokencookie, cache reading and writing are completely bypassed. Logged-in users always receive fresh, personalized SSR. -
Strict allowlist: Caching is restricted strictly to anonymous public pages (
/,/pricing,/about,/changelog,/tools/*,/blog/*, and legal pages). Dynamic routes (/dashboard,/api/*,/settings) are never cached. -
Cache write condition: Only responses with HTTP status
200,Content-Type: text/html, and noSet-Cookieheader are stored.
// src/server.ts (abridged)
const canCache =
request.method === 'GET' &&
!hasSessionCookie(request) &&
isCacheablePath(deLocalizeUrl(new URL(request.url)).pathname);
const cache = (caches as unknown as { default: Cache }).default;
const cacheKey = canCache ? edgeCacheKey(request) : request;
if (canCache) {
const hit = await cache.match(cacheKey);
if (hit) {
const headers = new Headers(hit.headers);
headers.set('X-Edge-Cache', 'HIT');
return new Response(hit.body, { status: hit.status, headers });
}
}
Solving the Stale Content Problem
The biggest danger of caches.default on Cloudflare Workers is that a new Worker deployment does not purge the cache. With a standard URL key, publishing a new blog post or fixing a bug would leave stale HTML served to users for days.
We solved this by injecting a compile-time build identifier into the cache key:
// vite.config.ts
export default defineConfig({
define: {
__EDGE_BUILD_ID__: JSON.stringify(Date.now().toString(36)),
},
// ...
});
In src/server.ts, we construct an internal cache key request tagged with __EDGE_BUILD_ID__:
function edgeCacheKey(request: Request): Request {
const url = new URL(request.url);
url.searchParams.set('__ev', __EDGE_BUILD_ID__);
return new Request(url.toString(), request);
}
This internal query parameter is only passed to cache.match and cache.put; it is never exposed to the client or upstream origin.
When a new version deploys, __EDGE_BUILD_ID__ changes. Old cache entries become instantly unreachable and expire naturally on their TTL, while the new release begins populating immediately. We can safely set s-maxage=86400 (24h) and stale-while-revalidate=604800 (7 days) without ever worrying about stale HTML after a release.
Result: Edge cache hits now return in under 45 milliseconds directly from the nearest Cloudflare edge PoP, completely bypassing Worker isolate cold starts.
3. Layer 2: Main Bundle Decoupling & Vendor Splitting
Serving HTML quickly is only half the battle; if the browser has to parse hundreds of kilobytes of unoptimized JavaScript before becoming interactive, the experience still stumbles.
Decoupling Global Configs
Our site configuration (src/config/website.ts) originally bundled metadata, navigation menus, multilingual pricing matrices, and 84 tool route names into one giant object. Because the root layout imported websiteConfig, every visitor downloaded definitions for 84 tools they hadn't visited.
We extracted tool metadata into src/config/navbar-tool-names.ts and pricing calculations into src/config/price-plans.ts, ensuring tool pages only load their own definitions on demand.
Vite Manual Chunks
By default, Vite bundled React, TanStack Query, and Zod into monolithic client bundles. In vite.config.ts, we configured explicit vendor chunks:
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('@tabler/icons-react')) return 'vendor-icons';
if (
id.includes('node_modules/react/') ||
id.includes('node_modules/react-dom/') ||
id.includes('node_modules/scheduler/')
) return 'vendor-react';
if (
id.includes('node_modules/@tanstack/react-query') ||
id.includes('node_modules/@tanstack/query-core')
) return 'vendor-query';
if (id.includes('node_modules/zod/')) return 'vendor-zod';
},
},
},
}
This isolates shared dependencies, improving long-term browser caching across route transitions and preventing small code changes from invalidating vendor libraries.
Scoping Global Providers
In src/routes/__root.tsx, Radix TooltipProvider originally wrapped the entire application tree. This forced React to initialize tooltip context on every public landing page even though tooltips were only used inside the logged-in dashboard and transcription editor. We removed TooltipProvider from the root route and scoped it exclusively to the editor and dashboard components.
Similarly, we extracted prose.css from the global styles.css. Markdown typography styling is now loaded strictly on blog, legal, and documentation routes, eliminating unused CSS overhead on marketing pages.
4. Layer 3: Offscreen Lazy-Loading & Slashing Hydration TBT
On mobile devices, client-side hydration was choking the CPU. When a browser downloads a page, hydrating every single DOM node on a long marketing page blocks user taps and scrolls (high Total Blocking Time).
Synchronous Above-the-Fold, Lazy Below-the-Fold
In src/components/blocks/homepage.tsx, we split the landing page into two categories:
-
Above-the-Fold (Synchronous):
HeroSection,TrustStrip, andWhisperTechSectionare imported synchronously. They render immediately during SSR and hydrate on frame 1 for instant First Contentful Paint (FCP). -
Below-the-Fold (Lazy Loaded):
FeaturesSection,Features2Section,StatsSection,CallToActionSection,PricingSection,FaqSection, andNewsletterCardare wrapped inReact.lazy()andSuspense.
To prevent Cumulative Layout Shift (CLS) when these components resolve, each Suspense boundary has an explicit minimum-height skeleton fallback matching the component's rendered height:
// src/components/blocks/homepage.tsx
export function HomePage() {
return (
<div className="flex flex-col">
<HeroSection />
<TrustStrip />
<WhisperTechSection />
<Suspense fallback={<div className="min-h-[650px]" />}>
<FeaturesSection />
</Suspense>
<Suspense fallback={<div className="min-h-[550px]" />}>
<Features2Section />
</Suspense>
<Suspense fallback={<div className="min-h-[300px]" />}>
<CallToActionSection />
</Suspense>
{/* ... */}
</div>
);
}
This guaranteed 0.00 CLS while cutting the initial hydration JavaScript payload by over 40%.
Deferring Google One Tap to Idle
One of the largest contributors to mobile Total Blocking Time was Google One Tap (authClient.oneTap()). Previously, the Google Identity Services script executed during initial React hydration, spinning up iframe bridges and network checks while the user was trying to interact with the hero section.
We moved Google One Tap out of the critical rendering path by wrapping it in requestIdleCallback (with an 8-second fallback timeout) and attaching one-time event listeners to user interactions (pointerdown, touchstart, scroll):
// src/routes/__root.tsx (abridged)
useEffect(() => {
if (!isOneTapEnabled || isPending || session) return;
let executed = false;
const trigger = () => {
if (executed) return;
executed = true;
cleanup();
void authClient.oneTap();
};
const interactionEvents = ['pointerdown', 'touchstart', 'scroll'] as const;
for (const evt of interactionEvents) {
window.addEventListener(evt, trigger, { once: true, passive: true });
}
if (typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(trigger, { timeout: 8000 });
} else {
setTimeout(trigger, 7000);
}
// ...
}, [isPending, session]);
The result: zero main-thread interference during initial paint.
5. Layer 4: LCP & Accessibility Polish
With TTFB and hydration fixed, we addressed visual rendering timing and audit metrics:
-
Critical CSS Preloading: Added
<link rel="preload" as="style" href={appCss} />insrc/routes/__root.tsxto eliminate stylesheet render blocking. -
Hero H1 Animation Delay Removal: In
src/components/blocks/hero.tsx, our primary H1 heading previously had a subtle entrance fade-in animation delay. Removing the delay allowed Lighthouse to record Largest Contentful Paint (LCP) the instant the first paint completed. -
Accessibility & Contrast: Fixed
aria-orientation="horizontal"on button toggle groups, added descriptivearia-labeltags, and increased primary button color contrast to meet WCAG AA standards. - Descriptive Anchor Text: Replaced ambiguous "Learn more" link texts with descriptive link destinations ("Security →"), satisfying Lighthouse SEO crawlability checks.
6. The Results: Lighthouse 95+ & Core Web Vitals
After deploying these four layers, we ran Google PageSpeed Insights on scribetoany.com.
Desktop Performance: 95 / 100
On desktop devices, our score surged to 95 Performance, 100 Accessibility, 96 Best Practices, and 100 SEO, with a 3/3 score on Agentic Browsing audits.
| Metric | Score / Value | Status |
|---|---|---|
| Performance | 95 | 🟢 Excellent |
| Accessibility | 100 | 🟢 Perfect |
| Best Practices | 96 | 🟢 Excellent |
| SEO | 100 | 🟢 Perfect |
| Agentic Browsing | 3 / 3 | 🟢 Passed |
| Cumulative Layout Shift (CLS) | 0.00 | 🟢 Zero Shift |
Mobile Performance: 78 / 100
On simulated mobile networks with aggressive 4G throttling and restricted mobile CPU profiles, our performance jumped to 78 Performance, alongside flawless 100 Accessibility and 100 SEO scores:
| Metric | Before Optimization | After Optimization |
|---|---|---|
| P75 Edge TTFB | ~3,128 ms (57% Poor) | < 50 ms (Edge HIT) |
| Cold Worker TTFB | ~3,500 ms | Bypassed for all anon visitors |
| Desktop Performance | ~68 | 95 🟢 |
| Mobile Performance | ~42 | 78 🟡 |
| Cumulative Layout Shift | 0.03 | 0.00 🟢 |
| Accessibility | 92 | 100 🟢 |
| SEO | 90 | 100 🟢 |
Key Lessons for Full-Stack Edge Applications
Building on Cloudflare Workers and a modern full-stack React framework gives you unprecedented global reach, but edge runtimes operate under different rules than traditional Node servers:
- Beware the "Fast Handler, Slow Isolate" trap: If your Worker takes 5ms of CPU time but bundle size is 10MB+, your users are waiting on V8 compilation, not code execution.
-
Edge Cache is mandatory for SSR: You don't need a static site generator (SSG) to get static speeds. Using Cloudflare's
caches.defaultwith build-ID key invalidation gives you static speed with all the flexibility of SSR. - Guard your cache keys with build tags: Never deploy edge caches without an automated invalidation strategy. Folding compile-time hashes into internal cache keys guarantees zero-downtime freshness.
-
Hydrate lazily, paint immediately: Render above-the-fold components synchronously, defer offscreen blocks with explicit height skeletons, and push heavy third-party scripts (like Google Identity Services) to
requestIdleCallback.
Speed is a feature. By aligning our edge architecture with browser execution priorities, ScribeToAny now delivers an instantaneous experience to users worldwide.


Top comments (0)