From e24db8a8897021fdfaf3300d6189d18e6c37d80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 7 Jul 2026 12:34:26 +0200 Subject: [PATCH] perf(i18n): stop shipping two full catalogs on every page load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent every-load costs, one per layer: 1. The static EN import in load-locale.ts put the full en.json catalog (334 KB raw / ~106 KB gz) into the client JS of EVERY route, for every user in every language. No catalog is statically imported by client code anymore: the EN fallback floor of the t() chain loads lazily on the first actual key miss (the locale-integrity guard keeps every locale key-complete, so the miss path is drift-only), and the labs biomarker canonical-name lookup fetches EN on demand. 2. The root layout serialized the ENTIRE active-locale catalog into the RSC flight payload of every document (392 KB of a 505 KB dashboard HTML, re-downloaded on every hard load, uncacheable). The handoff is now a versioned, immutable-cacheable boot script (/i18n/?v=, new route handler) that assigns self.__HL_I18N before the hydration scripts run, plus a server-side cache seed in load-locale.ts behind a typeof-window branch that Turbopack compiles out of the browser bundle (verified: zero catalog bytes in any client chunk, zero route manifests referencing one). SSR text stays correct and the first client render holds the same bundle the server rendered with — no EN flash, no hydration drift. The boot script is public (proxy allowlist: the login page needs it pre-auth; the body is the same catalog JSON that ships in the public repo) and cache-first in the service worker, so an offline relaunch finds it next to the cached shell. Measured (client-reference manifests, gzip): route JS baseline / 391->287 KB gz, /insights 377->273, /measurements 379->275, /insights/mood 394->290 — the ~104 KB gz EN catalog left every route. H2 --- public/sw.js | 11 +++ src/app/i18n/[locale]/route.ts | 51 ++++++++++ src/app/layout.tsx | 30 +++--- src/components/labs/lab-biomarker-detail.tsx | 43 ++++++--- src/components/providers.tsx | 8 +- src/lib/i18n/context.tsx | 49 ++++++++-- src/lib/i18n/load-locale.ts | 97 +++++++++++++++----- src/proxy.ts | 4 + 8 files changed, 232 insertions(+), 61 deletions(-) create mode 100644 src/app/i18n/[locale]/route.ts diff --git a/public/sw.js b/public/sw.js index 8c8141415..c1de63368 100644 --- a/public/sw.js +++ b/public/sw.js @@ -168,6 +168,17 @@ self.addEventListener("fetch", (event) => { return; } + // Locale-catalog boot script — cache-first. The URL is versioned + // (`/i18n/?v=`) and served immutable, so cache-first is + // correct and keeps the catalog available on an offline relaunch (the + // cached shell HTML references the same versioned URL it was rendered + // with). Carries no user data — same public strings as the repository's + // messages/*.json. + if (url.pathname.startsWith("/i18n/")) { + event.respondWith(cacheFirst(request, STATIC_CACHE)); + return; + } + // Static files (fonts, icons, images) — cache-first with long TTL if (/\.(png|svg|jpg|jpeg|gif|webp|ico|woff2?|ttf|eot)$/i.test(url.pathname)) { event.respondWith(cacheFirst(request, STATIC_CACHE)); diff --git a/src/app/i18n/[locale]/route.ts b/src/app/i18n/[locale]/route.ts new file mode 100644 index 000000000..244829fcb --- /dev/null +++ b/src/app/i18n/[locale]/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; + +import { locales, type Locale } from "@/lib/i18n/config"; +import { allMessages } from "@/lib/i18n/shared-resolve"; + +/** + * Locale-catalog boot script. + * + * The root layout emits `