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
@@ -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