From 0cd395db429b08477e2f5ef29cc37685d54635a7 Mon Sep 17 00:00:00 2001 From: Guide Fari Date: Fri, 10 Jul 2026 14:09:06 +0200 Subject: [PATCH 1/2] perf(www): cache index.html at edge, lazy-load auth dialogs Cold Lighthouse audit on goosebumps.fm showed LCP 1.9s despite prior bundle-size work (#139). Two contributors found: 1. index.html served with no-store, so Cloudflare/CloudFront never cache it at edge. Every cold visit pays a full origin round-trip (measured 829ms-1.3s TTFB) before the browser has HTML to parse. Give HTML a short edge TTL instead. 2. Root route eagerly imported AuthPromptDialog and WelcomeModal, both closed-by-default dialogs, forcing their chunks (and transitively use-toast/lucide icon code) into the 23 modulepreload tags fetched on every route load including home. Lazy-load them like QueueColumn already is in AppShell.tsx. --- apps/www/src/routes/__root.tsx | 17 ++++++++++++----- infra/www.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/www/src/routes/__root.tsx b/apps/www/src/routes/__root.tsx index 1dd88b8d..69dd3328 100644 --- a/apps/www/src/routes/__root.tsx +++ b/apps/www/src/routes/__root.tsx @@ -2,15 +2,20 @@ import { Toaster } from '@gbfm/ui' import { FPSMeter } from '@overengineering/fps-meter' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRootRouteWithContext, HeadContent, Outlet } from '@tanstack/react-router' -import { Suspense } from 'react' +import { lazy, Suspense } from 'react' import { VerifyEmailBanner } from '@/components/Auth/VerifyEmailBanner' -import { AuthPromptDialog } from '@/components/AuthPromptDialog' import { ErrorBoundary } from '@/components/ErrorBoundary' import AppShell from '@/components/Layout/AppShell' import { OfflineBanner } from '@/components/OfflineBanner' -import { WelcomeModal } from '@/components/onboarding/WelcomeModal' import { ThemeProvider } from '@/components/ThemeProvider' import { env } from '@/env' + +const AuthPromptDialog = lazy(() => + import('@/components/AuthPromptDialog').then((m) => ({ default: m.AuthPromptDialog })) +) +const WelcomeModal = lazy(() => + import('@/components/onboarding/WelcomeModal').then((m) => ({ default: m.WelcomeModal })) +) export interface MyRouterContext { auth: { user: { @@ -61,8 +66,10 @@ export const Route = createRootRouteWithContext()({ )} - - + + + + diff --git a/infra/www.ts b/infra/www.ts index cf3c666d..bacc843b 100644 --- a/infra/www.ts +++ b/infra/www.ts @@ -9,6 +9,18 @@ export const www = new sst.aws.StaticSite('gbfm-www', { command: 'bun run build', output: 'dist' }, + assets: { + fileOptions: [ + { + files: '**', + cacheControl: 'max-age=31536000,public,immutable' + }, + { + files: '**/*.html', + cacheControl: 'public,max-age=60,must-revalidate' + } + ] + }, environment: { VITE_VPS_BASE_URL: isLocal ? '' : vps_gateway.url, VITE_PUBLIC_SENTRY_DSN: secret.VITE_PUBLIC_SENTRY_DSN.value, From c447fe47b681cc80f1eae6df7bdeee7f08f4d8d5 Mon Sep 17 00:00:00 2001 From: Guide Fari Date: Fri, 10 Jul 2026 19:43:05 +0200 Subject: [PATCH 2/2] perf(www): migrate static site from AWS S3+CloudFront to Cloudflare Workers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DNS already lives on Cloudflare (proxy: true), so S3+CloudFront behind the Cloudflare edge was a redundant double-CDN hop, adding latency and a second cache layer to reason about (directly complicating the prior commit's index.html edge-cache change). Swap sst.aws.StaticSite for sst.cloudflare.StaticSiteV2 (Workers + static assets, SST's current recommended pattern per examples/cloudflare-vite). Also drops CloudFront/ACM/S3 provisioning and AWS credentials from the www deploy path. StaticSiteV2 has no assets.fileOptions API — Workers Static Assets defaults every file to 'public, max-age=0, must-revalidate'. Replaced with a Cloudflare-native apps/www/public/_headers file, copied verbatim into dist/ by Vite, to keep the same immutable long-cache for hashed assets/fonts and 60s revalidate for HTML. --- apps/www/public/_headers | 8 ++++++++ infra/www.ts | 15 +-------------- sst-env.d.ts | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) create mode 100644 apps/www/public/_headers diff --git a/apps/www/public/_headers b/apps/www/public/_headers new file mode 100644 index 00000000..b7790619 --- /dev/null +++ b/apps/www/public/_headers @@ -0,0 +1,8 @@ +/assets/* + Cache-Control: public, max-age=31536000, immutable + +/fonts/* + Cache-Control: public, max-age=31536000, immutable + +/*.html + Cache-Control: public, max-age=60, must-revalidate diff --git a/infra/www.ts b/infra/www.ts index bacc843b..d56b4220 100644 --- a/infra/www.ts +++ b/infra/www.ts @@ -3,24 +3,12 @@ import { secret } from './secret' import { isLocal } from './stage' import { vps_gateway } from './vps' -export const www = new sst.aws.StaticSite('gbfm-www', { +export const www = new sst.cloudflare.StaticSiteV2('gbfm-www', { path: './apps/www', build: { command: 'bun run build', output: 'dist' }, - assets: { - fileOptions: [ - { - files: '**', - cacheControl: 'max-age=31536000,public,immutable' - }, - { - files: '**/*.html', - cacheControl: 'public,max-age=60,must-revalidate' - } - ] - }, environment: { VITE_VPS_BASE_URL: isLocal ? '' : vps_gateway.url, VITE_PUBLIC_SENTRY_DSN: secret.VITE_PUBLIC_SENTRY_DSN.value, @@ -30,7 +18,6 @@ export const www = new sst.aws.StaticSite('gbfm-www', { }, domain: { name: `www.${domain}`, - dns: sst.cloudflare.dns({ proxy: true }), ...($app.stage === 'prod' ? { aliases: [domain] } : {}) } }) diff --git a/sst-env.d.ts b/sst-env.d.ts index 8189062f..f02e8a0c 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -99,7 +99,7 @@ declare module "sst" { "value": string } "gbfm-www": { - "type": "sst.aws.StaticSite" + "type": "sst.cloudflare.StaticSite" "url": string } "gbfm_network": {