From 8e211b64af781af9c461ea71dc5ba9075fe52f0b Mon Sep 17 00:00:00 2001 From: attacker Date: Fri, 17 Jul 2026 01:49:25 +0800 Subject: [PATCH] =?UTF-8?q?perf(frontend):=20U48=20i18n=20locale=20?= =?UTF-8?q?=E6=87=92=E5=8A=A0=E8=BD=BD=20=E2=80=94=20en-US=20JSON=20?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=AF=BC=E5=85=A5=E6=8B=86=E5=88=86=E4=B8=BA?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=20chunk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: 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 --- frontend/src/i18n/index.ts | 35 ++++++++++++++++++++++++++++++++--- frontend/src/store/index.ts | 5 +++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 7f2d303..4ed806d 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -1,7 +1,6 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import zhCN from '@/locales/zh-CN.json'; -import enUS from '@/locales/en-US.json'; 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'; +/** Track which locale resource bundles have been registered with i18next. */ +const loadedLocales = new Set(['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 { + 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) */ function getPersistedLanguage(): AppLanguage { try { @@ -27,12 +44,15 @@ function getPersistedLanguage(): AppLanguage { return DEFAULT_LANGUAGE; } +const persistedLang = getPersistedLanguage(); + i18n.use(initReactI18next).init({ resources: { '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', interpolation: { 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 with current language i18n.on('languageChanged', (lng) => { if (typeof document !== 'undefined') { diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 14b9db6..fcd3271 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -29,9 +29,10 @@ export const useLanguageStore = create()( (set) => ({ language: "zh-CN" as AppLanguage, setLanguage: (lang) => { - // 同步切换 i18next 实例 + // Ensure the target locale bundle is loaded (en-US is lazy) before switching. import("@/i18n") - .then(({ default: i18n }) => { + .then(async ({ default: i18n, ensureLocaleLoaded }) => { + await ensureLocaleLoaded(lang); i18n.changeLanguage(lang); }) .catch(() => {