perf: move the 19 non-zh/en dictionaries into a lazy locale chunk

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).
This commit is contained in:
Menghuan1918
2026-09-01 16:01:34 +08:00
parent b7af498427
commit c7d58c9b25
8 changed files with 100 additions and 69 deletions
+1 -1
View File
@@ -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). */
+1 -1
View File
@@ -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<string, unknown>
+60
View File
@@ -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<CopyKey, string>): Record<string, string> => 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<string, Record<string, string>> = {
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),
}
+19 -8
View File
@@ -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<string, Record<string, string>>)
})
.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)
+7 -43
View File
@@ -845,49 +845,13 @@ export const en: Record<keyof typeof zh, string> = {
*/
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<keyof typeof zh, string> = jaDict as Record<keyof typeof zh, string>
export const de: Record<keyof typeof zh, string> = deDict as Record<keyof typeof zh, string>
export const fr: Record<keyof typeof zh, string> = frDict as Record<keyof typeof zh, string>
export const pt: Record<keyof typeof zh, string> = ptDict as Record<keyof typeof zh, string>
export const ko: Record<keyof typeof zh, string> = koDict as Record<keyof typeof zh, string>
export const ar: Record<keyof typeof zh, string> = arDict as Record<keyof typeof zh, string>
export const hi: Record<keyof typeof zh, string> = hiDict as Record<keyof typeof zh, string>
export const id: Record<keyof typeof zh, string> = idDict as Record<keyof typeof zh, string>
export const tr: Record<keyof typeof zh, string> = trDict as Record<keyof typeof zh, string>
export const vi: Record<keyof typeof zh, string> = viDict as Record<keyof typeof zh, string>
export const th: Record<keyof typeof zh, string> = thDict as Record<keyof typeof zh, string>
export const ru: Record<keyof typeof zh, string> = ruDict as Record<keyof typeof zh, string>
export const it: Record<keyof typeof zh, string> = itDict as Record<keyof typeof zh, string>
export const nl: Record<keyof typeof zh, string> = nlDict as Record<keyof typeof zh, string>
export const sv: Record<keyof typeof zh, string> = svDict as Record<keyof typeof zh, string>
export const pl: Record<keyof typeof zh, string> = plDict as Record<keyof typeof zh, string>
export const zhHK: Record<keyof typeof zh, string> = zhHKDict as Record<keyof typeof zh, string>
export const zhTW: Record<keyof typeof zh, string> = zhTWDict as Record<keyof typeof zh, string>
export const zhMO: Record<keyof typeof zh, string> = zhMODict as Record<keyof typeof zh, string>
// 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
+1 -1
View File
@@ -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'])
})
})
+10 -14
View File
@@ -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<keyof typeof zh, string>` 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<string, string>).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)
+1 -1
View File
@@ -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 [
{