i18n 效能優化:如何透過 Dynamic Import 減少 70% 的 Bundle Size
深入解析 React 動態載入技術,將大型翻譯檔分割載入,提升網站初始載入速度。附實作程式碼分析與最佳實踐。
在開發多語言網站時,隨著翻譯內容的增加,翻譯檔(Translation JSON/Objects)往往會成為 Bundle Size 膨脹的元兇。本文將以本站的 Cheatsheets 功能為例,分析如何將大型 JSON 資料進行 Code Splitting(程式碼分割),從而顯著提升效能。
問題背景
在本站的 Cheatsheets 頁面中,我們收錄了 Git, Docker, Linux, K8s 等大量的速查表內容。這些內容包含詳細的指令、說明文字以及多語言版本。
最初的實作方式是將所有翻譯資料放在一個 translations.ts 檔案中:
// src/i18n/translations.ts (原始版本 - 約 135KB)
export const translations = {
'zh-TW': {
common: { ... }, // 核心 UI 字串
// 巨大的 Cheatsheets 資料,包含數百個指令
cheatsheets: {
categories: [ ... ]
}
},
'en': { ... }
}
這樣做的後果是:即使使用者只是瀏覽首頁,不需要查看 Cheatsheets,這 100KB+ 的資料也會被打包進主 Bundle 中下載。這不僅浪費頻寬,也延緩了 Hydration 的時間。
優化策略:Dynamic Import
解決方案是將「非初始載入必要」的大型資料拆分出去,只在使用者真正訪問該頁面或該組件時才載入。
1. 拆分資料檔
首先,我們將龐大的 Cheatsheets 資料從主翻譯檔中移出,獨立成 translations-cheatsheets.ts:
// src/i18n/translations.ts (瘦身後 - 約 30KB)
export const translations = {
'zh-TW': {
common: { ... },
cheatsheets: {
title: '速查表', // 只保留標題等 UI 骨架
// categories 陣列已移除
}
}
}
// src/i18n/translations-cheatsheets.ts (獨立檔案 - 約 85KB)
export const cheatsheetCategories = {
'zh-TW': [ ... ],
'en': [ ... ]
}
2. 組件端實作動態載入
在 React 組件 CheatsheetTabs.tsx 中,我們不再依賴從父層 Props 傳入完整的資料,而是改為在 useEffect 中動態 import:
// src/components/CheatsheetTabs.tsx
useEffect(() => {
const loadCategories = async () => {
setLoading(true);
try {
// 🚀 關鍵優化:動態載入大型資料
// Webpack/Vite 會自動將其分割為獨立的 Chunk
const mod = await import('../i18n/translations-cheatsheets');
// 取得對應語言的資料
const loadedCategories = mod.cheatsheetCategories[currentLang] || [];
setCategories(loadedCategories);
} catch (error) {
console.error('Download failed', error);
} finally {
setLoading(false);
}
};
loadCategories();
}, [currentLang]); // 當語言改變時重新載入
技術分析
這種方式是什麼?
這就是 Code Splitting (程式碼分割)。
在打包工具(如 Vite, Webpack)遍歷程式碼時,當它遇到靜態的 import (import ... from ...),它會將其打包在一起。但當它遇到 import() 函數時,它知道這是一個非同步操作,因此會將被引用的模組及其依賴打包成一個 獨立的 JavaScript Chunk(例如 translations-cheatsheets-D8x2a.js)。
瀏覽器在初始載入時不會下載這個 Chunk,只有當 import() 被執行時(即組件 render 且 useEffect 執行時),瀏覽器才會發起網路請求去下載這個檔案。
優缺點比較
| 項目 | 靜態載入 (Static Import) | 動態載入 (Dynamic Import) |
|---|---|---|
| Bundle Size | 🔴 大 (包含所有資料) | 🟢 小 (只包含核心程式碼) |
| 初始載入速度 | 🔴 較慢 | 🟢 較快 |
| 互動體驗 | 🟢 即時顯示 | 🟡 需等待非同步載入 (需 Loading 狀態) |
| 實作複雜度 | 🟢 低 | 🟡 中 (需處理 Async/State) |
最佳實踐
- Skeleton Loading:由於動態載入需要網路請求,使用者可能會看到短暫的空白。務必實作 Skeleton Screen(骨架畫面)或 Loading Spinner 來優化 UX。
- 保留核心 UI:不要把所有東西都拆分。頁面的標題 (
Title)、描述 (Subtitle) 等應該保留在主 Bundle 中,這樣在資料載入期間,頁面框架依然是完整的,不會發生劇烈的 Layout Shift(版面位移)。 - 錯誤處理:網路請求可能會失敗,必須用
try-catch包裹import()並提供錯誤 UI。 - 類型安全:拆分檔案後,TypeScript 的類型推斷可能會變弱。建議定義共享的 Interface(如
CheatSheetCategory)來確保資料結構的一致性。
結論
透過這個簡單的重構,我們成功將主翻譯檔的大小減少了約 75% (135KB -> 30KB)。對於內容型網站或包含大量靜態資料的應用來說,將這類資料移出主執行緒並採用動態載入,是提升 Core Web Vitals 效能最立竿見影的手段之一。