mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
perf(i18n): stop shipping two full catalogs on every page load
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/<locale>?v=<build>, 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
This commit is contained in:
1 parent
e260b2d5c4
commit
e24db8a889
8 files changed
+232
-61
No files matched your search
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
@@ -27,7 +27,11 @@ import { apiDelete, apiGet } from "@/lib/api/api-fetch";
|
||||
import { summarize, type DataSummary } from "@/lib/analytics/trends";
|
||||
import { BIOMARKER_CATALOG } from "@/lib/labs/biomarker-catalog";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { fallbackMessages } from "@/lib/i18n/load-locale";
|
||||
import {
|
||||
getFallbackMessages,
|
||||
loadMessages,
|
||||
type MessageBundle,
|
||||
} from "@/lib/i18n/load-locale";
|
||||
import { resolveKey } from "@/lib/i18n/resolve-key";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
@@ -92,23 +96,40 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
|
||||
// marker minted under the English default and then viewed in another locale
|
||||
// never matched a map built only from the current locale's names — the rich
|
||||
// description silently fell through to the generic line. The English bundle
|
||||
// is always in the client chunk (`fallbackMessages`), so adding its names
|
||||
// makes resolution independent of the locale the marker was created in.
|
||||
// resolves through `getFallbackMessages()` (already in hand on the server
|
||||
// and for EN sessions; lazily fetched below otherwise — the EN catalog is
|
||||
// no longer a static every-route import), so adding its names makes
|
||||
// resolution independent of the locale the marker was created in.
|
||||
const [enBundle, setEnBundle] = useState<MessageBundle | undefined>(
|
||||
getFallbackMessages,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (enBundle) return;
|
||||
let cancelled = false;
|
||||
void loadMessages("en")
|
||||
.then((loaded) => {
|
||||
if (!cancelled) setEnBundle(loaded);
|
||||
})
|
||||
.catch(() => {
|
||||
// Offline — the current-locale names still resolve; only markers
|
||||
// minted under another locale keep the generic description.
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [enBundle]);
|
||||
const slugByName = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const seed of BIOMARKER_CATALOG) {
|
||||
const localized = t(`labs.catalog.${seed.slug}`).trim().toLowerCase();
|
||||
if (localized) map.set(localized, seed.slug);
|
||||
const canonical = resolveKey(
|
||||
fallbackMessages,
|
||||
`labs.catalog.${seed.slug}`,
|
||||
)
|
||||
?.trim()
|
||||
.toLowerCase();
|
||||
const canonical = enBundle
|
||||
? resolveKey(enBundle, `labs.catalog.${seed.slug}`)?.trim().toLowerCase()
|
||||
: undefined;
|
||||
if (canonical && !map.has(canonical)) map.set(canonical, seed.slug);
|
||||
}
|
||||
return map;
|
||||
}, [t]);
|
||||
}, [t, enBundle]);
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
// Sticky-footer slot for the add-value sheet (the form portals here).
|
||||
const [addFooterEl, setAddFooterEl] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -182,12 +182,9 @@ function QueryPersistenceBridge() {
|
||||
export function Providers({
|
||||
children,
|
||||
initialLocale,
|
||||
initialMessages,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
initialLocale?: Locale;
|
||||
/** Active locale's bundle, server-resolved by the root layout (RSC handoff). */
|
||||
initialMessages?: Record<string, unknown>;
|
||||
}) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
@@ -206,10 +203,7 @@ export function Providers({
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<I18nProvider
|
||||
initialLocale={initialLocale}
|
||||
initialMessages={initialMessages}
|
||||
>
|
||||
<I18nProvider initialLocale={initialLocale}>
|
||||
<QueryPersistenceBridge />
|
||||
<DashboardSnapshotPreloader />
|
||||
{children}
|
||||
|
||||
Reference in new issue
Block a user