perf(i18n): stop serializing the full catalogue on public pages
The root layout fed the entire ~254 KB message catalogue to NextIntlClientProvider, which serializes it into the RSC payload of every page. A public page only reads ~59 KB of it; the ~195 KB difference is staff tool copy that only /admin, /admin-next and /mod ever render. The root provider now installs the reduced catalogue and each staff route group re-installs the full one, since a nested NextIntlClientProvider replaces the parent message set rather than merging into it. pages.admin.actions stays public because the shared useServerAction hook runs on public pages too. Measured: 253 KB -> 59 KB of messages per public page (raw JSON). Guard tests cover all three halves: the root keeps using the reduced catalogue, every staff layout installs its own full provider, and no public tree reads a staff-only namespace.
This commit is contained in:
1 parent
d28dde459a
commit
bb3799f705
7 files changed
+136
-18
No files matched your search
@@ -85,7 +85,12 @@ describe("publicClientMessages", () => {
|
||||
if (key === "admin") continue;
|
||||
expect(result[key]).toBeDefined();
|
||||
}
|
||||
expect(result.pages.admin).toBeUndefined();
|
||||
// `pages.admin` survives only in the shape the shared server-action hook
|
||||
// needs: the generic `actions` copy, nothing staff-specific.
|
||||
expect(Object.keys(result.pages.admin)).toEqual(["actions"]);
|
||||
expect(result.pages.admin.actions.saved).toBe(en.pages.admin.actions.saved);
|
||||
expect(result.pages.admin.nav).toBeUndefined();
|
||||
expect(result.pages.admin.catalog).toBeUndefined();
|
||||
});
|
||||
|
||||
it("actually shrinks the catalogue", async () => {
|
||||
@@ -111,3 +116,59 @@ describe("publicClientMessages", () => {
|
||||
expect(en.pages.admin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Scoping only works if the reduced catalogue goes to the client *and* every
|
||||
* staff route group re-installs the full one. These two halves drift silently:
|
||||
* narrowing the root without adding a staff provider renders raw keys in the
|
||||
* admin panel, and the reverse just costs bytes. Both are asserted here.
|
||||
*/
|
||||
describe("client catalogue scoping is wired up", () => {
|
||||
const STAFF_LAYOUTS = [
|
||||
"src/app/admin/layout.tsx",
|
||||
"src/app/admin-next/layout.tsx",
|
||||
"src/app/mod/layout.tsx",
|
||||
];
|
||||
|
||||
it("the root layout feeds the reduced catalogue to its provider", () => {
|
||||
const src = readFileSync(join(process.cwd(), "src/app/layout.tsx"), "utf8");
|
||||
expect(src).toContain("publicClientMessages(await getMessages())");
|
||||
});
|
||||
|
||||
for (const layout of STAFF_LAYOUTS) {
|
||||
it(`${layout} re-installs the full catalogue`, () => {
|
||||
const src = readFileSync(join(process.cwd(), layout), "utf8");
|
||||
expect(src).toContain("NextIntlClientProvider");
|
||||
expect(src).toContain("getMessages()");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A client component that reads `admin.*` / `pages.admin.*` outside a staff
|
||||
* route group would fall back to raw keys once the root catalogue is scoped,
|
||||
* because the provider it inherits no longer carries them. Staff trees are the
|
||||
* ones that install the full catalogue; the allowlist holds shared code that
|
||||
* deliberately runs on public pages too.
|
||||
*/
|
||||
describe("staff namespace consumers sit in a staff tree", () => {
|
||||
const STAFF_TREE =
|
||||
/^src[\\/](app[\\/](admin|admin-next|mod)[\\/]|components[\\/]admin[\\/]|features[\\/](catalog|operations|history|housekeeping)[\\/])/;
|
||||
const ALLOWLIST = new Set(["src/hooks/use-server-action.ts"]);
|
||||
const CLIENT_STAFF_NS = /useTranslations\((["`])((admin)|(pages\.admin))/;
|
||||
|
||||
it("there is source to scan", () => {
|
||||
expect(sourceFiles(join(process.cwd(), "src")).length).toBeGreaterThan(500);
|
||||
});
|
||||
|
||||
it("no stray consumer of a staff namespace", () => {
|
||||
const offenders: string[] = [];
|
||||
for (const file of sourceFiles(join(process.cwd(), "src"))) {
|
||||
const rel = file.slice(process.cwd().length + 1).replaceAll("\\", "/");
|
||||
if (rel.includes(".test.")) continue;
|
||||
if (STAFF_TREE.test(rel) || ALLOWLIST.has(rel)) continue;
|
||||
if (CLIENT_STAFF_NS.test(readFileSync(file, "utf8"))) offenders.push(rel);
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user