Vue3 – 效能與最佳化:Code Splitting(程式碼分割)
簡介
在單頁應用(SPA)中,所有的 JavaScript 都會在首次載入時一次性下載,隨著功能變多、元件增長,首次載入時間、互動延遲 以及 記憶體佔用 都會急速上升。即使使用了 Vue3 的 Composition API、Tree‑shaking 等現代化特性,若未對路由或大型元件進行 code splitting(程式碼分割),仍會讓使用者感受到卡頓。
Code splitting 能把應用程式切成多個較小的 bundle,讓瀏覽器只在需要時才載入對應的程式碼。這不只縮短了首屏渲染時間,也降低了資源浪費,對 SEO、行動裝置效能提升尤為顯著。本文將從概念說明、實作範例、常見陷阱到最佳實踐,帶你一步步在 Vue3 專案中掌握程式碼分割的技巧。
核心概念
1. 為什麼需要 Code Splitting
- 首屏加速:只載入當前路由所需的程式碼,減少下載大小。
- 懶載入 (Lazy Loading):配合 Vue Router 動態載入元件,讓使用者在切換頁面時才下載。
- 快取效益:分割後的 bundle 較小,瀏覽器快取更有效,更新時只需要重新下載變動的檔案。
2. Webpack / Vite 中的分割策略
| 分割方式 | 說明 | 典型使用情境 |
|---|---|---|
| Entry Point Splitting | 依照入口檔 (main.js) 自動產生 vendor、app 等 bundle。 |
基本專案結構 |
Dynamic Import (import()) |
在程式碼執行時才載入模組,產生 懶載入 chunk。 | 路由或大型元件 |
| Route‑level Splitting | 在 Vue Router 中直接使用 component: () => import('...')。 |
多頁面 SPA |
| Component‑level Splitting | 在單一頁面內部使用 defineAsyncComponent 包裝元件。 |
需要在同頁面中條件渲染的重型元件 |
重點:Vue3 官方建議使用 動態匯入 搭配 Vue Router 做路由層級的程式碼分割,因為這樣最符合使用者的瀏覽行為。
3. 動態匯入與 defineAsyncComponent
- 動態匯入 (
import()):返回一個 Promise,Webpack/Vite 會自動產生相對應的 chunk。 defineAsyncComponent:Vue3 提供的 API,讓你以組件形式使用懶載入,支援 loading、error、timeout 等狀態處理。
程式碼範例
範例 1:路由層級的 Code Splitting(最常見)
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue') // <-- 動態匯入
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue') // <-- 動態匯入
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue') // 大型頁面
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
說明:每個路由都會產生獨立的 chunk,只有在使用者切換到該路由時才會下載對應的 JavaScript。
範例 2:在同一頁面內部使用 defineAsyncComponent 懶載入子元件
// src/components/HeavyChart.vue
<template>
<div class="chart">Loading chart...</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const Chart = defineAsyncComponent({
// 1. 匯入的元件
loader: () => import('@/components/ChartLibrary.vue'),
// 2. 載入期間的占位元件
loadingComponent: {
template: '<div class="spinner">載入中…</div>'
},
// 3. 錯誤時的占位元件
errorComponent: {
template: '<div class="error">載入失敗,請稍後再試。</div>'
},
// 4. 超時設定 (ms)
timeout: 30000
})
</script>
<template>
<Chart />
</template>
說明:
ChartLibrary.vue可能依賴大量圖表套件(如 Chart.js、ECharts),使用defineAsyncComponent可避免首次載入時把它一起打包。
範例 3:手動設定 Chunk 名稱,方便除錯與快取
// src/router/index.js
const routes = [
{
path: '/profile',
name: 'Profile',
component: () => import(/* webpackChunkName: "profile" */ '@/views/Profile.vue')
},
{
path: '/settings',
name: 'Settings',
component: () => import(/* webpackChunkName: "settings" */ '@/views/Settings.vue')
}
]
說明:在 Webpack 中加入
/* webpackChunkName: "xxx" */註解,可自訂產生的檔名,對於 Cache‑busting 與 分析 bundle 大小 非常有幫助。Vite 也支援類似的/* @vite-ignore */註解。
範例 4:使用 Vite 的 import.meta.glob 產生自動化路由與分割
// src/router/autoRoutes.js (Vite)
import { createRouter, createWebHistory } from 'vue-router'
// 透過 glob 取得所有 view 檔案,並自動產生路由
const modules = import.meta.glob('../views/**/*.vue')
const routes = Object.keys(modules).map(path => {
const name = path
.replace('../views/', '')
.replace('.vue', '')
.replace(/\//g, '-')
return {
path: `/${name}`,
name,
component: modules[path] // <-- Vite 會自動把每個檔案當作懶載入
}
})
export default createRouter({
history: createWebHistory(),
routes
})
說明:
import.meta.glob會返回一組 懶載入函式,每個路由自動對應一個 chunk,適合大型專案的 自動化路由生成。
範例 5:結合 Suspense 與 defineAsyncComponent 提升使用者體驗
// src/App.vue
<template>
<Suspense>
<template #default>
<HeavyChart />
</template>
<template #fallback>
<div class="loader">載入圖表中…</div>
</template>
</Suspense>
</template>
<script setup>
import HeavyChart from '@/components/HeavyChart.vue' // 已使用 defineAsyncComponent
</script>
說明:
Suspense讓開發者可以在異步元件載入期間顯示自訂的 loading UI,提升感知效能。
常見陷阱與最佳實踐
| 陷阱 | 可能的影響 | 解決方案 / 最佳實踐 |
|---|---|---|
| 過度拆分 | 產生過多小 chunk,導致 HTTP 請求次數激增,反而拖慢載入速度。 | 依功能模組或路由層級拆分,避免在同一頁面內部頻繁使用 import()。 |
| 同步 vs. 非同步混用 | 同一個元件同時被同步與非同步引用,會產生兩個 bundle(重複代碼)。 | 確保元件只以一種方式匯入,若需要全域使用,考慮在入口文件中同步匯入。 |
| 預先載入失效 | 使用 prefetch/preload 時未正確設定,導致資源在不需要時就被下載。 |
在路由設定中使用 webpackPrefetch: true(Webpack)或 rel="prefetch" 於 <link>,僅對預測會在短時間內使用的 chunk。 |
| 錯誤處理缺失 | 懶載入失敗(網路斷線)時,使用者會看到空白或無回應。 | 在 defineAsyncComponent 中提供 errorComponent、timeout,或在路由守衛捕獲 import() 的 reject。 |
| 快取失效 | 每次部署都改變 chunk 名稱,導致瀏覽器無法利用快取。 | 使用固定的 webpackChunkName,或在生產環境使用內容雜湊(content‑hash)作為檔名。 |
最佳實踐總結
- 路由層級分割:所有主要頁面皆使用
component: () => import(...)。 - 大型或第三方套件:使用
defineAsyncComponent包裝,搭配Suspense提供 loading UI。 - Chunk 命名:為重要的 chunk 加上易讀名稱,方便除錯與快取策略。
- 預先載入 (Prefetch):對於使用者可能很快會點擊的路由,可使用
webpackPrefetch: true讓瀏覽器在空閒時下載。 - 監控與分析:利用 Chrome DevTools、Webpack Bundle Analyzer 或 Vite 的
visualizer,定期檢視 bundle 大小與 chunk 分布。
實際應用場景
1. 電商平台的商品列表與商品詳情
- 商品列表:資料量大,但 UI 較簡單,使用同步載入。
- 商品詳情:包含圖片輪播、評論、即時庫存等多個子模組,使用路由層級懶載入,僅在點擊商品後才下載。
2. SaaS 後台管理系統
- Dashboard:圖表與儀表板通常依賴重型圖表庫,使用
defineAsyncComponent懶載入圖表元件,並透過Suspense顯示 loading。 - 設定頁面:多數設定項目屬於表單,體積小,可直接同步載入,避免不必要的 chunk 切分。
3. 行動端 PWA(Progressive Web App)
- 首屏:只保留最核心的入口與登入流程,確保在低速網路下仍能快速啟動。
- 功能模組:如離線筆記、相機上傳等功能分割成獨立 chunk,僅在使用者觸發時才下載,降低流量消耗。
4. 多語系或多租戶平台
- 語系檔案:使用
import(/* webpackChunkName: "lang-[request]" */./locales/${lang}.js)動態載入語系資源,避免一次載入所有語言檔案。 - 租戶客製化:每個租戶的品牌樣式或插件可分割為獨立 chunk,根據租戶 ID 在路由守衛中載入對應資源。
總結
Code splitting 是 Vue3 應用在 效能優化 方面最直接、最有效的手段之一。透過 路由層級懶載入、defineAsyncComponent、以及 Vite/Webpack 的自訂 Chunk,我們可以:
- 大幅縮短首次載入時間,提升使用者的第一印象。
- 減少不必要的資源下載,降低行動網路流量消耗。
- 讓快取策略更精細,更新時只需要重新下載變更的部分。
在實務開發中,切記 適度分割、完善錯誤處理、以及 持續監控 bundle 大小,才能在保持開發效率的同時,提供使用者流暢且快速的體驗。希望本篇文章能幫助你在 Vue3 專案中順利導入程式碼分割,打造更快、更輕量的前端應用! 🚀