本文 AI 產出,尚未審核

Vue3 – 效能與最佳化:Lazy Loading 元件

簡介

在單頁應用(SPA)中,所有元件一次性載入會導致 首屏渲染時間 大幅增加、記憶體佔用過高,尤其在行動裝置上更容易出現卡頓。
Vue 3 提供的 lazy loading(懶加載)機制,讓我們可以把不立即需要的元件延遲載入,僅在使用者真正需要時才下載,從而顯著降低初始載入體積、提升使用者體驗。

本篇文章將從概念、實作、常見陷阱與最佳實踐,帶你一步步掌握 Vue 3 中的元件懶加載,並提供實務案例,讓你在專案中快速應用。


核心概念

1. 為什麼要 Lazy Load 元件

  • 減少首屏資源:只載入當前路由或視圖所需的元件,其他元件等到使用者點擊或滾動時才載入。
  • 提升互動速度:載入的 JavaScript 檔案變小,瀏覽器解析、執行的時間縮短。
  • 降低帶寬成本:對於使用 CDN 的大型網站,懶加載可減少不必要的流量。

2. Vue 3 的懶加載語法

Vue 3 使用 defineAsyncComponent(在 vue 包內)或 動態 import 搭配 <router-view> 來實現非同步元件。

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)

上述寫法會在 AsyncComp 被渲染時,自動觸發 import(),返回一個 Promise,待模組載入完成後再渲染。

3. 路由層面的懶加載

在 Vue Router 中,我們常把每個路由的 component 設為動態 import,這樣整個路由的程式碼都會分割成獨立的 chunk。

const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  },
  {
    path: '/settings',
    component: () => import('@/views/Settings.vue')
  }
]

這樣做的好處是 只有使用者真的切換到 /dashboard 時,才會下載 Dashboard 的程式碼

4. 結合 Suspense 提供載入狀態

Vue 3 引入的 <Suspense> 元素讓我們可以在非同步元件載入期間顯示備援 UI(例如 Loading spinner)。

<Suspense>
  <template #default>
    <AsyncComp />
  </template>
  <template #fallback>
    <div class="loading">載入中…</div>
  </template>
</Suspense>

這樣使用者不會看到空白畫面,體驗更友好。

5. 設定載入超時與錯誤處理

defineAsyncComponent 接受第二個參數,可自訂 loadingComponenterrorComponenttimeout 等。

const AsyncComp = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: LoadError,
  delay: 200,          // 200ms 後才顯示 loadingComponent
  timeout: 3000        // 超過 3 秒視為失敗
})

透過這些選項,我們可以更細緻地控制使用者在網路不佳時的感受。


程式碼範例

範例 1:最簡單的元件懶加載

// src/components/Chart.vue
export default {
  name: 'Chart',
  template: `<div>這是一個圖表元件</div>`
}

// 其他元件中使用
import { defineAsyncComponent } from 'vue'

export default {
  components: {
    AsyncChart: defineAsyncComponent(() => import('./components/Chart.vue'))
  },
  template: `
    <button @click="show = !show">切換圖表</button>
    <AsyncChart v-if="show" />
  `,
  data() {
    return { show: false }
  }
}

說明AsyncChart 只在 show 變為 true 時才會觸發載入。


範例 2:結合 Suspense 與自訂載入/錯誤 UI

// LoadingSpinner.vue
<template><div class="spinner">載入中…</div></template>

// LoadError.vue
<template><div class="error">載入失敗,請稍後再試。</div></template>

<script>
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from '@/components/LoadingSpinner.vue'
import LoadError from '@/components/LoadError.vue'

export default {
  components: {
    HeavyComp: defineAsyncComponent({
      loader: () => import('@/components/HeavyComp.vue'),
      loadingComponent: LoadingSpinner,
      errorComponent: LoadError,
      delay: 300,
      timeout: 5000
    })
  },
  template: `
    <Suspense>
      <template #default>
        <HeavyComp />
      </template>
      <template #fallback>
        <LoadingSpinner />
      </template>
    </Suspense>
  `
}
</script>

說明:即使 loader 超時或失敗,使用者也會看到友善的錯誤訊息。


範例 3:路由層面的懶加載與分割 Chunk

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')   // 產生 home.[hash].js
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('@/views/Profile.vue') // 產生 profile.[hash].js
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'), // 產生 admin.[hash].js
    meta: { requiresAuth: true }
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})

說明:每個視圖都會被打包成獨立的 chunk,只有在對應路由被觸發時才會下載。


範例 4:動態載入大型第三方套件(如 Chart.js)

// ChartWrapper.vue
<template>
  <canvas ref="canvas"></canvas>
</template>

<script>
import { defineAsyncComponent, onMounted, ref } from 'vue'

export default {
  setup() {
    const canvas = ref(null)

    onMounted(async () => {
      const { default: Chart } = await import('chart.js')   // 只在需要時載入
      new Chart(canvas.value, {
        type: 'bar',
        data: { /* ... */ },
        options: { /* ... */ }
      })
    })

    return { canvas }
  }
}
</script>

說明:即使整個應用使用了 Chart.js,透過上述寫法,只有在渲染 ChartWrapper 時才會載入該套件,節省大量程式碼。


範例 5:自訂懶加載函式,支援多語系或主題切換

// utils/lazyLoadComponent.js
import { defineAsyncComponent } from 'vue'

export function lazyLoad(path) {
  return defineAsyncComponent({
    loader: () => import(/* @vite-ignore */ path),
    loadingComponent: () => import('@/components/LoadingSpinner.vue'),
    errorComponent: () => import('@/components/LoadError.vue'),
    delay: 100,
    timeout: 4000
  })
}

// 使用方式
export default {
  components: {
    LocaleComp: lazyLoad(`@/components/${i18n.locale}/SpecialFeature.vue`)
  }
}

說明:根據當前語系動態決定要載入哪個元件,避免一次打包所有語系檔案。


常見陷阱與最佳實踐

陷阱 可能的影響 解決方式
忘記設定 delay 使用者會瞬間看到 loading UI,造成閃爍感 設定 delay(如 200ms)讓小於此時間的載入直接顯示內容
過度切割(過細的 Chunk) 請求次數過多,反而拖慢載入速度 依功能或路由劃分,保持每個 chunk 大小在 30~100KB(視專案而定)
懶加載的元件內部仍引用大量同步依賴 懶加載效果被抵消 確保懶加載元件本身的依賴也使用 import() 或放在同一 chunk 中
使用 defineAsyncComponent 時未處理錯誤 下載失敗會導致空白或程式崩潰 加入 errorComponenttimeout,或在 loader 中捕捉例外
SSR 環境未考慮 伺服器端渲染會因為懶加載而失去預渲染優勢 在 SSR 時使用 ssr: truedefineAsyncComponent,或在伺服器端直接同步載入

最佳實踐

  1. 先分析流量與使用者行為:哪些頁面/元件是「首次必須」的,哪些是「次要」的。
  2. 以路由為單位切割:大多數情況下,路由懶加載已能滿足需求。
  3. 統一載入指示 UI:使用 <Suspense> 或全局 loading component,保持 UI 一致。
  4. 設定合理的 timeout & delay:避免因網路差導致永遠卡住 loading。
  5. 使用 Vite/webpack 的 Chunk 命名webpackChunkName 或 Vite 的 import(/* webpackChunkName: "admin" */ './Admin.vue'),方便後續監控與快取。

實際應用場景

場景 為何使用懶加載
大型儀表板(Dashboard) 圖表、報表等元件往往載入大量第三方套件,僅在使用者點擊相應卡片時才需要載入。
多語系/多主題網站 每個語系或主題的資源可分成獨立 chunk,切換時再動態載入,避免一次載入所有語系檔案。
電商平台的商品詳情頁 商品圖片輪播、評論區、相關商品推薦等可分別懶加載,提升首次渲染速度。
管理後台的權限模組 非所有使用者都會使用管理功能,將管理相關路由與元件延遲載入,可減少普通使用者的下載量。
PWA 離線模式 先載入核心功能,其他功能等網路恢復後再預先快取,提升離線體驗。

總結

  • Lazy loading 是提升 Vue 3 應用效能的關鍵手段,能顯著降低首屏載入時間與記憶體佔用。
  • 透過 defineAsyncComponent、路由的動態 import(),以及 <Suspense>,我們可以在保持良好使用者體驗的同時,靈活控制程式碼分割。
  • 實作時要注意 delay、timeout、錯誤處理,避免因過度切割或未處理失敗而產生負面效果。
  • 結合實際業務需求(如大型儀表板、多語系、電商商品頁)進行策略性懶加載,才能真正發揮效能優化的價值。

掌握了上述概念與技巧,你就能在 Vue 3 專案中自信地使用懶加載,讓應用跑得更快、體驗更好。祝開發順利!