Skip to content
Anonymous

i18n 效能優化:如何透過 Dynamic Import 減少 70% 的 Bundle Size

深入解析 React 動態載入技術,將大型翻譯檔分割載入,提升網站初始載入速度。附實作程式碼分析與最佳實踐。

#react #performance #i18n #astro #optimization

在開發多語言網站時,隨著翻譯內容的增加,翻譯檔(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)

最佳實踐

  1. Skeleton Loading:由於動態載入需要網路請求,使用者可能會看到短暫的空白。務必實作 Skeleton Screen(骨架畫面)或 Loading Spinner 來優化 UX。
  2. 保留核心 UI:不要把所有東西都拆分。頁面的標題 (Title)、描述 (Subtitle) 等應該保留在主 Bundle 中,這樣在資料載入期間,頁面框架依然是完整的,不會發生劇烈的 Layout Shift(版面位移)。
  3. 錯誤處理:網路請求可能會失敗,必須用 try-catch 包裹 import() 並提供錯誤 UI。
  4. 類型安全:拆分檔案後,TypeScript 的類型推斷可能會變弱。建議定義共享的 Interface(如 CheatSheetCategory)來確保資料結構的一致性。

結論

透過這個簡單的重構,我們成功將主翻譯檔的大小減少了約 75% (135KB -> 30KB)。對於內容型網站或包含大量靜態資料的應用來說,將這類資料移出主執行緒並採用動態載入,是提升 Core Web Vitals 效能最立竿見影的手段之一。