perf(frontend): U48 i18n locale 懒加载 — en-US JSON 动态导入拆分为独立 chunk

问题: src/i18n/index.ts 静态导入 zh-CN.json (200KB compact) + en-US.json
(292KB compact),两者共 486KB 全部内联到主 index chunk,致主 chunk 696KB
超过 Vite 600KB 警告阈值。

修复:
- src/i18n/index.ts: 移除 en-US.json 静态导入,改为 ensureLocaleLoaded()
  动态导入 + i18n.addResourceBundle() 按需加载
- zh-CN (默认/回退语言) 保持静态内联,无行为变化
- en-US 仅在用户切换英文时按需 fetch (290KB 独立 chunk)
- 已选英文的用户刷新页面时异步加载 en-US (短暂中文闪烁,可接受)
- src/store/index.ts: setLanguage 调用 ensureLocaleLoaded 确保目标语言包
  加载完成后再切换

结果:
- 主 index chunk: 696KB → 423KB (-39%, -273KB)
- en-US chunk: 290KB 独立懒加载 (仅英文用户首次切换时 fetch)
- Vite 600KB chunk 警告消除
- tsc -b=0, npm build✓ (18.18s), ESLint=0/43/any=0, go build=0
- 17/17 集成测试通过, Docker healthy
This commit is contained in:
attacker committed 2026-07-17 01:49:25 +08:00
1 parent ca036a85ca
commit 8e211b64af
2 files changed
+35 -5

No files matched your search

+32 -3
View File
@@ -1,7 +1,6 @@
import i18n from 'i18next'; import i18n from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import zhCN from '@/locales/zh-CN.json'; import zhCN from '@/locales/zh-CN.json';
import enUS from '@/locales/en-US.json';
export type AppLanguage = 'zh-CN' | 'en-US'; export type AppLanguage = 'zh-CN' | 'en-US';
@@ -12,6 +11,24 @@ export const SUPPORTED_LANGUAGES: { code: AppLanguage; label: string; flag: stri
export const DEFAULT_LANGUAGE: AppLanguage = 'zh-CN'; export const DEFAULT_LANGUAGE: AppLanguage = 'zh-CN';
/** Track which locale resource bundles have been registered with i18next. */
const loadedLocales = new Set<string>(['zh-CN']);
/**
* Ensure a locale resource bundle is loaded before use.
* zh-CN is bundled inline (default/fallback language).
* en-US is lazy-loaded via dynamic import to reduce the main bundle by ~290 kB.
* Subsequent calls for an already-loaded locale are instant no-ops.
*/
export async function ensureLocaleLoaded(lang: string): Promise<void> {
if (loadedLocales.has(lang)) return;
if (lang === 'en-US') {
const mod = await import('@/locales/en-US.json');
i18n.addResourceBundle('en-US', 'translation', mod.default, true, true);
loadedLocales.add('en-US');
}
}
/** Read persisted language from localStorage (zustand persist key) */ /** Read persisted language from localStorage (zustand persist key) */
function getPersistedLanguage(): AppLanguage { function getPersistedLanguage(): AppLanguage {
try { try {
@@ -27,12 +44,15 @@ function getPersistedLanguage(): AppLanguage {
return DEFAULT_LANGUAGE; return DEFAULT_LANGUAGE;
} }
const persistedLang = getPersistedLanguage();
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources: { resources: {
'zh-CN': { translation: zhCN }, 'zh-CN': { translation: zhCN },
'en-US': { translation: enUS },
}, },
lng: getPersistedLanguage(), // Start with zh-CN if the persisted language (en-US) hasn't been loaded yet.
// en-US is fetched asynchronously below and the language is switched once ready.
lng: persistedLang === 'en-US' ? 'zh-CN' : persistedLang,
fallbackLng: 'zh-CN', fallbackLng: 'zh-CN',
interpolation: { interpolation: {
escapeValue: false, // React already escapes escapeValue: false, // React already escapes
@@ -42,6 +62,15 @@ i18n.use(initReactI18next).init({
}, },
}); });
// If the user previously selected en-US, load it asynchronously then switch.
// There is a brief flash of Chinese before English loads — acceptable trade-off
// for a ~290 kB reduction in the main entry chunk.
if (persistedLang === 'en-US') {
void ensureLocaleLoaded('en-US').then(() => {
i18n.changeLanguage('en-US');
});
}
// Sync <html lang="..."> with current language // Sync <html lang="..."> with current language
i18n.on('languageChanged', (lng) => { i18n.on('languageChanged', (lng) => {
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
+3 -2
View File
@@ -29,9 +29,10 @@ export const useLanguageStore = create<LanguageState>()(
(set) => ({ (set) => ({
language: "zh-CN" as AppLanguage, language: "zh-CN" as AppLanguage,
setLanguage: (lang) => { setLanguage: (lang) => {
// 同步切换 i18next 实例 // Ensure the target locale bundle is loaded (en-US is lazy) before switching.
import("@/i18n") import("@/i18n")
.then(({ default: i18n }) => { .then(async ({ default: i18n, ensureLocaleLoaded }) => {
await ensureLocaleLoaded(lang);
i18n.changeLanguage(lang); i18n.changeLanguage(lang);
}) })
.catch(() => { .catch(() => {