From c7d58c9b2531a5445cf55d4bea2f48458f8abc3a Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Tue, 1 Sep 2026 00:34:57 +0800 Subject: [PATCH] perf: move the 19 non-zh/en dictionaries into a lazy locale chunk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The core client bundle statically imported every third-language dictionary (ja/de/fr/…/zh-HK/MO/TW, ~640KB of source — roughly half the bundle's weight) for a code path most sessions never enter: the sidebar's own t() only consults zh/en plus the better-locale override store, and the dicts' ONLY consumer is that store's register() call. They now ride the existing lazy-chunk machinery as lib/client-locale.js (fetched from /sidebar/bundle only when better-locale is actually installed; a generation counter keeps the async registration moot when the integration effect re-runs or disposes mid-flight). Until the chunk lands, t() keeps the zh/en chain; the store's own revision bump on register re-renders the chrome once the dicts arrive. Core client.js: 1.46MB → 822KB (-44%). The chunk-boundary key-set check moves to the existing runtime test (every shipped dict stays key-set-equal to zh). --- src/bundle-route.ts | 2 +- src/client/chunk-loader.ts | 2 +- src/client/chunks/locale.tsx | 60 ++++++++++++++++++++++++++++++++++++ src/client/index.tsx | 27 +++++++++++----- src/client/locales.ts | 50 +++++------------------------- tests/bundle-route.spec.ts | 2 +- tests/locales.spec.ts | 24 ++++++--------- tsdown.config.ts | 2 +- 8 files changed, 100 insertions(+), 69 deletions(-) create mode 100644 src/client/chunks/locale.tsx diff --git a/src/bundle-route.ts b/src/bundle-route.ts index 3c404f3..4f098ec 100644 --- a/src/bundle-route.ts +++ b/src/bundle-route.ts @@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url' import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts' /** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */ -export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid'] as const +export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid', 'locale'] as const export type ChunkName = (typeof CHUNK_NAMES)[number] /** Directory of this host-half module (lib/ — the chunk scripts live next to it). */ diff --git a/src/client/chunk-loader.ts b/src/client/chunk-loader.ts index 76c90e1..169da7c 100644 --- a/src/client/chunk-loader.ts +++ b/src/client/chunk-loader.ts @@ -49,7 +49,7 @@ * client.js); an edit that does land while a core HMR happens is caught by * the ETag comparison on the next activation. */ -export type ChunkName = 'terminal' | 'editor' | 'mermaid' +export type ChunkName = 'terminal' | 'editor' | 'mermaid' | 'locale' /** The module exports a chunk factory provides (namespace-ish record). */ export type ChunkExports = Record diff --git a/src/client/chunks/locale.tsx b/src/client/chunks/locale.tsx new file mode 100644 index 0000000..b2a439e --- /dev/null +++ b/src/client/chunks/locale.tsx @@ -0,0 +1,60 @@ +/** + * Lazy chunk entry: the 19 non-zh/en dictionaries (ja/de/fr/…/zh-HK/MO/TW — + * ~640KB of source). The sidebar's own `t()` only ever consults zh/en plus + * the better-locale override store, so these dicts are needed ONLY when + * @huanlin/dsh-plugin-better-locale is installed: they register into its + * override store from there (see the client apply's better-locale + * integration). Built as `lib/client-locale.js` and fetched from the + * plugin's /sidebar/bundle route on first need — never import this module + * from the core bundle: it would drag every language back into the startup + * path (see docs/plans/2026-08-31-perf-optimization.md). + */ +import { ja } from '../locales-ja.ts' +import { de } from '../locales-de.ts' +import { fr } from '../locales-fr.ts' +import { pt } from '../locales-pt.ts' +import { ko } from '../locales-ko.ts' +import { ar } from '../locales-ar.ts' +import { hi } from '../locales-hi.ts' +import { id } from '../locales-id.ts' +import { tr } from '../locales-tr.ts' +import { vi } from '../locales-vi.ts' +import { th } from '../locales-th.ts' +import { ru } from '../locales-ru.ts' +import { it } from '../locales-it.ts' +import { nl } from '../locales-nl.ts' +import { sv } from '../locales-sv.ts' +import { pl } from '../locales-pl.ts' +import { zhHK } from '../locales-zh-HK.ts' +import { zhTW } from '../locales-zh-TW.ts' +import { zhMO } from '../locales-zh-MO.ts' +import type { CopyKey } from '../locales.ts' + +/** Key-set check against zh (type-only import — erased before bundling, so + * this adds no runtime tie to the core bundle): a dictionary missing or + * adding a key fails the build instead of silently falling back to en. */ +const checked = (dict: Record): Record => dict + +/** Every non-zh/en dictionary keyed by its override id, ready for the + * better-locale store's `register(ns, dicts)`. */ +export const localeDicts: Record> = { + ja: checked(ja), + de: checked(de), + fr: checked(fr), + pt: checked(pt), + ko: checked(ko), + ar: checked(ar), + hi: checked(hi), + id: checked(id), + tr: checked(tr), + vi: checked(vi), + th: checked(th), + ru: checked(ru), + it: checked(it), + nl: checked(nl), + sv: checked(sv), + pl: checked(pl), + 'zh-HK': checked(zhHK), + 'zh-TW': checked(zhTW), + 'zh-MO': checked(zhMO), +} diff --git a/src/client/index.tsx b/src/client/index.tsx index 7ba5b31..ff25c8a 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -24,10 +24,8 @@ import { registerSettingsNavIcon } from './settings-nav-icon.ts' import { loadBootDecision } from './prefs.ts' import { SideCardSection } from './SideCardSection.tsx' import { api } from './api.ts' -import { LOCALE_NS, attachLocale, attachBetterLocale, t, zh, en, - ja, de, fr, pt, ko, ar, hi, id, tr, vi, th, ru, it, nl, sv, pl, - zhHK, zhTW, zhMO, -} from './locales.ts' +import { LOCALE_NS, attachLocale, attachBetterLocale, t, zh, en } from './locales.ts' +import { loadChunk } from './chunk-loader.ts' import css from './sidebar.module.css' import './layout.css' @@ -82,7 +80,12 @@ export function apply(ctx: Context): void { // the ja dict once the store becomes available. ctx.effect(() => { let dispose: (() => void) | undefined + // Guards the async chunk registration below: a sync() re-run (or fiber + // disposal) that lands while the chunk is still in flight must render + // that registration moot. + let generation = 0 const sync = (): void => { + generation += 1 dispose?.() dispose = undefined const store = ctx.get('betterLocale') as @@ -96,10 +99,17 @@ export function apply(ctx: Context): void { | undefined attachBetterLocale(store) if (store !== undefined) { - dispose = store.register(LOCALE_NS, { - ja, de, fr, pt, ko, ar, hi, id, tr, vi, th, ru, it, nl, sv, pl, - 'zh-HK': zhHK, 'zh-TW': zhTW, 'zh-MO': zhMO, - }) + // The 19 override dictionaries ride the lazy `locale` chunk: until + // it lands, the store has no betterSidebar entries and t() keeps + // the zh/en chain; the store's own revision bump on register + // re-renders the chrome once the dicts arrive. + const myGeneration = generation + void loadChunk('locale') + .then(mod => { + if (myGeneration !== generation) return + dispose = store.register(LOCALE_NS, mod.localeDicts as Record>) + }) + .catch(() => { /* the dicts stay unregistered; the zh/en chain runs */ }) } } // Initial check (picks up the store if better-locale activated first). @@ -108,6 +118,7 @@ export function apply(ctx: Context): void { // activates with a persisted override, and when the user switches). const unsubscribe = ctx.locale.subscribe(sync) return () => { + generation += 1 unsubscribe() dispose?.() attachBetterLocale(undefined) diff --git a/src/client/locales.ts b/src/client/locales.ts index 584a7b7..4177b6b 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -845,49 +845,13 @@ export const en: Record = { */ export const LOCALE_NS = 'betterSidebar' -// The ja dictionary lives in a sibling file (326 keys) so this module -// stays readable. Type-checked against the zh key set: a missing or extra -// ja key is a compile error. -import { ja as jaDict } from './locales-ja.ts' -import { de as deDict } from './locales-de.ts' -import { fr as frDict } from './locales-fr.ts' -import { pt as ptDict } from './locales-pt.ts' -import { ko as koDict } from './locales-ko.ts' -import { ar as arDict } from './locales-ar.ts' -import { hi as hiDict } from './locales-hi.ts' -import { id as idDict } from './locales-id.ts' -import { tr as trDict } from './locales-tr.ts' -import { vi as viDict } from './locales-vi.ts' -import { th as thDict } from './locales-th.ts' -import { ru as ruDict } from './locales-ru.ts' -import { it as itDict } from './locales-it.ts' -import { nl as nlDict } from './locales-nl.ts' -import { sv as svDict } from './locales-sv.ts' -import { pl as plDict } from './locales-pl.ts' -import { zhHK as zhHKDict } from './locales-zh-HK.ts' -import { zhTW as zhTWDict } from './locales-zh-TW.ts' -import { zhMO as zhMODict } from './locales-zh-MO.ts' - -/** The ja dictionary (key-set-equal to zh, enforced by the type annotation). */ -export const ja: Record = jaDict as Record -export const de: Record = deDict as Record -export const fr: Record = frDict as Record -export const pt: Record = ptDict as Record -export const ko: Record = koDict as Record -export const ar: Record = arDict as Record -export const hi: Record = hiDict as Record -export const id: Record = idDict as Record -export const tr: Record = trDict as Record -export const vi: Record = viDict as Record -export const th: Record = thDict as Record -export const ru: Record = ruDict as Record -export const it: Record = itDict as Record -export const nl: Record = nlDict as Record -export const sv: Record = svDict as Record -export const pl: Record = plDict as Record -export const zhHK: Record = zhHKDict as Record -export const zhTW: Record = zhTWDict as Record -export const zhMO: Record = zhMODict as Record +// The 19 non-zh/en dictionaries live in the LAZY `locale` chunk +// (src/client/chunks/locale.tsx → lib/client-locale.js): `t()` only ever +// consults zh/en plus the better-locale override store, so those dicts are +// needed only when better-locale is installed — the client apply fetches the +// chunk there and registers its `localeDicts` into the override store. They +// used to sit statically in the core bundle (≈ half its weight) for a code +// path most sessions never enter. /** The DSH locale service attached by the client apply (absent → browser detection). */ let localeService: { getSnapshot(): { active: string } } | undefined diff --git a/tests/bundle-route.spec.ts b/tests/bundle-route.spec.ts index faf4400..1585cd5 100644 --- a/tests/bundle-route.spec.ts +++ b/tests/bundle-route.spec.ts @@ -145,6 +145,6 @@ describe('/sidebar/bundle route', () => { }) it('exports the chunk allowlist (mirror of src/client/chunk-loader.ts)', () => { - expect([...CHUNK_NAMES]).toEqual(['terminal', 'editor', 'mermaid']) + expect([...CHUNK_NAMES]).toEqual(['terminal', 'editor', 'mermaid', 'locale']) }) }) diff --git a/tests/locales.spec.ts b/tests/locales.spec.ts index 4e12ffe..e2e6cab 100644 --- a/tests/locales.spec.ts +++ b/tests/locales.spec.ts @@ -6,7 +6,8 @@ * interpolation. */ import { afterEach, describe, expect, it } from 'vitest' -import { LOCALE_NS, attachBetterLocale, attachLocale, en, isZh, ja, relativeTime, t, zh, de, fr, pt, ko, ar, hi, id as idDict, tr, vi, th, ru, it as itDict, nl, sv, pl, zhHK, zhTW, zhMO } from '../src/client/locales.ts' +import { LOCALE_NS, attachBetterLocale, attachLocale, en, isZh, relativeTime, t, zh } from '../src/client/locales.ts' +import { localeDicts } from '../src/client/chunks/locale.tsx' /** Minimal structural fake of the DSH LocaleService face the sidebar uses. */ class FakeLocale { @@ -108,20 +109,15 @@ describe('locales (DSH i18n following)', () => { expect(Object.keys(en).sort()).toEqual(Object.keys(zh).sort()) }) - it('keeps the ja dictionary key-set-equal to zh', () => { - expect(Object.keys(ja).sort()).toEqual(Object.keys(zh).sort()) - }) - it('keeps every shipped third-language dictionary key-set-equal to zh', () => { - // The third-language dicts are typed `Record` via - // an `as` cast (unchecked by tsc). This test catches missing/extra keys - // introduced when a zh key is added without updating every language. - const thirdLangs = { - de, fr, pt, ko, ar, hi, idDict, tr, vi, th, ru, itDict, nl, sv, pl, zhHK, zhTW, zhMO, - } - for (const [lang, dict] of Object.entries(thirdLangs)) { - expect(Object.keys(dict as Record).sort(), lang).toEqual(Object.keys(zh).sort()) + // The third-language dicts live in the lazy locale chunk typed as plain + // records (the chunk boundary has no key-set type tie to zh). This test + // catches missing/extra keys introduced when a zh key is added without + // updating every language — a missing key silently falls back to en. + for (const [lang, dict] of Object.entries(localeDicts)) { + expect(Object.keys(dict).sort(), lang).toEqual(Object.keys(zh).sort()) } + expect(Object.keys(localeDicts), 'every shipped third language rides the locale chunk').toContain('ja') }) }) @@ -247,7 +243,7 @@ describe('locales (better-locale override)', () => { locale.switchTo('en') attachLocale(locale) - const betterLocale = new FakeBetterLocale({ [LOCALE_NS]: { ja } }) + const betterLocale = new FakeBetterLocale({ [LOCALE_NS]: { ja: localeDicts.ja! } }) betterLocale.active = 'ja' attachBetterLocale(betterLocale) diff --git a/tsdown.config.ts b/tsdown.config.ts index 2183fd9..881a45d 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -324,7 +324,7 @@ function makeCssPlugin(pluginId: string): BuildPlugin { } /** The lazy chunk names (keep in sync with src/bundle-route.ts CHUNK_NAMES). */ -const CHUNKS = ['terminal', 'editor', 'mermaid'] +const CHUNKS = ['terminal', 'editor', 'mermaid', 'locale'] export default [ {