本文 AI 產出,尚未審核

Vue3 – 效能與最佳化:Code Splitting(程式碼分割)

簡介

在單頁應用(SPA)中,所有的 JavaScript 都會在首次載入時一次性下載,隨著功能變多、元件增長,首次載入時間互動延遲 以及 記憶體佔用 都會急速上升。即使使用了 Vue3 的 Composition APITree‑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:結合 SuspensedefineAsyncComponent 提升使用者體驗

// 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 中提供 errorComponenttimeout,或在路由守衛捕獲 import() 的 reject。
快取失效 每次部署都改變 chunk 名稱,導致瀏覽器無法利用快取。 使用固定的 webpackChunkName,或在生產環境使用內容雜湊(content‑hash)作為檔名。

最佳實踐總結

  1. 路由層級分割:所有主要頁面皆使用 component: () => import(...)
  2. 大型或第三方套件:使用 defineAsyncComponent 包裝,搭配 Suspense 提供 loading UI。
  3. Chunk 命名:為重要的 chunk 加上易讀名稱,方便除錯與快取策略。
  4. 預先載入 (Prefetch):對於使用者可能很快會點擊的路由,可使用 webpackPrefetch: true 讓瀏覽器在空閒時下載。
  5. 監控與分析:利用 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 專案中順利導入程式碼分割,打造更快、更輕量的前端應用! 🚀