Vue 打包與資源優化
Vue 專案的性能問題,不只發生在畫面更新時,也可能發生在使用者還沒看到畫面之前:bundle 太大、第三方庫太重、圖片字體太肥、快取策略錯誤、source map 暴露、chunk 拆得不合理,都會拖慢首屏與後續路由切換。
一句話回答:打包與資源優化的核心,是減少首屏必須下載與執行的資源,讓非必要程式碼延後載入,並透過壓縮、CDN、快取與依賴治理降低傳輸和解析成本;常見手段包含路由懶載入、組件懶載入、Tree Shaking、第三方庫按需導入、chunk 拆分、gzip/brotli、hash 檔名與 source map 策略。
一、 必考觀念
1. 打包優化解決什麼問題?
打包與資源優化不只是讓 build 變快,它更關心使用者端:
| 問題 | 影響 |
|---|---|
| JS 太大 | 下載慢、解析慢、執行慢 |
| CSS 太大 | 下載慢、可能阻塞渲染 |
| 圖片太重 | LCP 慢、流量浪費 |
| 字體太重 | 文字顯示延遲、可能造成版面跳動 |
| chunk 拆分不合理 | 首屏太重或切頁時卡 |
| 快取策略錯誤 | 每次都重新下載,或使用者拿到舊資源 |
| source map 處理不當 | 產物過大或暴露原始碼 |
所以打包優化要回答三個問題:
少下載什麼?
晚下載什麼?
下載後能不能快取?2. 不要只看檔案大小,也要看執行成本
同樣是 300KB,有些庫只是工具函數,有些庫會在載入後做大量初始化。
要同時看:
- transfer size:網路傳輸大小。
- parsed size:解壓後大小。
- parse / compile time:瀏覽器解析 JS 的時間。
- execute time:執行初始化的時間。
- 是否阻塞首屏。
因此 CDN、gzip、brotli 可以降低傳輸成本,但不能消除 JS parse/execute 成本。
3. 先用工具分析,不要憑感覺猜
常用工具:
- Chrome DevTools Network。
- Chrome DevTools Coverage。
- Lighthouse。
rollup-plugin-visualizer。- webpack-bundle-analyzer。
- Vite build report 或 Rollup 輸出。
要先知道:
- 主包多大?
- 哪些依賴最大?
- 有沒有重複依賴?
- 哪些頁面進了首屏 bundle?
- 哪些 chunk 被過早下載?
- gzip/brotli 後大小是多少?
沒有 bundle analyzer 的優化,很多時候只是在猜。
二、 分包與懶載入
1. 路由懶載入
Vue Router 最常見的分包方式是路由懶載入:
const routes = [
{
path: '/',
component: () => import('@/views/HomePage.vue'),
},
{
path: '/admin',
component: () => import('@/views/AdminPage.vue'),
},
]這樣 AdminPage 不會進入首頁首屏必須下載的主包。
適合懶載入:
- 後台管理頁。
- 低頻功能頁。
- 大型報表頁。
- 使用者點進去才需要的頁面。
延伸讀:Vue 路由實現:Hash 路由與 History 路由原理。
2. 組件懶載入
不是只有路由可以懶載入,重型組件也可以:
import { defineAsyncComponent } from 'vue'
const RichEditor = defineAsyncComponent(() => {
return import('@/components/RichEditor.vue')
})適合:
- 富文本編輯器。
- 圖表。
- 地圖。
- PDF 預覽。
- 程式碼編輯器。
- 非首屏 Modal。
如果某個 Modal 很少打開,就沒有必要一進頁面就把它的所有程式碼下載下來。
3. chunk 不是越碎越好
分包有副作用。
chunk 太大:
- 首屏下載重。
- 初次 parse / execute 慢。
chunk 太碎:
- 請求數變多。
- 路由切換可能等很多小檔。
- 快取命中與載入順序更難控制。
- HTTP/1.1 環境下更明顯。
比較好的原則是:
- 首屏必要程式碼放首屏。
- 低頻功能延後。
- 大型第三方庫獨立出來。
- 共用依賴穩定快取。
- 不要為了拆而拆。
4. 預取與預載要克制
可以在使用者即將需要某個路由時預取 chunk。
例如:
- hover 選單時預取。
- 首屏空閒後預取高機率下一頁。
- 登入後預取常用頁面。
但不要把所有路由都 prefetch。那會搶首屏頻寬,讓「懶載入」失去意義。
三、 Tree Shaking 與按需導入
1. Tree Shaking 是什麼?
Tree Shaking 是打包工具移除未使用程式碼的能力。
它通常更依賴 ESM:
export function add(a: number, b: number) {
return a + b
}
export function multiply(a: number, b: number) {
return a * b
}如果只使用 add:
import { add } from './math'打包工具有機會移除未使用的 multiply。
2. CommonJS 對 Tree Shaking 較不友好
CommonJS 是執行時模組系統,靜態分析比較困難。
const utils = require('./utils')相比之下,ESM 的 import / export 是靜態結構,打包工具更容易分析哪些 export 沒被使用。
面試時可以說:
Tree Shaking 的效果和模組格式、打包工具、套件的 side effects 標記、實際 import 寫法都有關,不是只要開 build 就一定完全生效。
3. 工具庫按需導入
不理想:
import _ from 'lodash'更好:
import debounce from 'lodash-es/debounce'或使用支援 ESM 與按需導入的套件。
常見要注意的依賴:
- 工具庫。
- 日期庫。
- UI library。
- icon library。
- 圖表庫。
- 地圖 SDK。
- 富文本編輯器。
4. UI library 與 icon 要避免全量引入
不理想:
import ElementPlus from 'element-plus'
app.use(ElementPlus)如果專案只用少量組件,應考慮按需引入或自動導入。
icon 也一樣,不要為了幾個圖示把整包 icon 全打進來。
這類問題用 bundle analyzer 最容易看出來。
四、 第三方依賴治理
1. 找出真正的大依賴
常見大依賴:
- 圖表庫。
- 地圖 SDK。
- 富文本編輯器。
- PDF / Excel 解析。
- 日期與時區處理。
- 語法高亮。
- 程式碼編輯器。
處理方式:
- 是否能按需載入?
- 是否能換更小的套件?
- 是否能延後到使用者操作後載入?
- 是否能放到獨立 chunk?
- 是否真的需要這個功能?
2. 避免重複依賴
例如專案裡同時出現多個版本:
lodash@4.17.x
lodash-es@4.17.x
dayjs
moment或 A 套件帶一份日期庫,B 套件又帶另一份。
這時可以檢查:
- lockfile。
- bundle analyzer。
- package manager dedupe。
- 是否能統一套件版本。
- 是否能移除重疊功能。
3. 日期庫、圖表庫、編輯器要特別小心
例如 moment 常見問題是語系包與時區資料可能很大。
如果只是簡單格式化日期,可能不需要引入重型方案。可以考慮:
- 原生
Intl.DateTimeFormat。 - dayjs。
- date-fns。
- 只引入必要 locale。
圖表與編輯器則通常適合動態載入:
async function openEditor() {
const { createEditor } = await import('@/features/editor/createEditor')
createEditor()
}這樣使用者沒有打開編輯器時,就不用支付下載與初始化成本。
五、 傳輸、壓縮與快取
1. gzip 與 brotli 解決什麼?
gzip / brotli 解決的是傳輸大小。
原始 JS: 900KB
gzip 後: 260KB
brotli 後: 220KB但瀏覽器下載後仍然要解壓、解析、編譯與執行 JS。
所以:
- gzip/brotli 可以讓下載更快。
- 不能解決 JS 執行太久。
- 不能替代分包與依賴治理。
2. CDN 解決什麼?
CDN 主要解決:
- 使用者離資源伺服器太遠。
- 靜態資源下載慢。
- 原站壓力過大。
- 快取命中率不足。
適合放 CDN:
- JS/CSS 靜態產物。
- 圖片。
- 字體。
- 可公開快取的靜態資源。
但如果瓶頸是 API 慢或 JS 執行慢,CDN 只能解決其中一部分。
3. hash 檔名與快取策略
打包後常見產物:
assets/index.8f3a2c1.js
assets/vendor.91ab33d.css檔名帶 hash 的好處是:內容變了,檔名也變。
典型快取策略:
index.html: no-cache 或短快取
assets/*.js: max-age=31536000, immutable
assets/*.css: max-age=31536000, immutable
assets/*.{png,jpg,webp,avif,woff2}: 長快取為什麼 index.html 不適合長快取?
因為它是資源入口。如果它被長快取,使用者可能一直拿到舊的 JS/CSS 路徑。
4. 圖片與字體也是資源優化
圖片常常比 JS 更大。
優化方向:
- 使用 WebP / AVIF。
- 使用正確尺寸。
- 非首屏圖片 lazy loading。
- 首屏 LCP 圖片適度 preload 或提高 priority。
- 圖片 CDN 動態裁切。
- SVG icon 按需載入。
字體優化:
- 只載入需要的字重。
- 使用
font-display: swap。 - 子集化字體。
- 避免過多 webfont。
六、 Vite 與 webpack 優化差異
1. Vite 的開發與生產不同
Vite 開發環境主要利用瀏覽器原生 ESM,並用 esbuild 做依賴預打包。
生產建置則使用 Rollup 打包。
所以 Vite 優化常見方向:
- 檢查依賴預打包。
- 使用 dynamic import。
- Rollup
manualChunks。 - 依賴按需導入。
- 圖片與靜態資源處理。
- bundle visualizer。
2. webpack 常見優化方向
webpack 專案常見方向:
splitChunks拆公共依賴。- 持久化 cache。
- loader 範圍縮小。
- thread-loader。
- babel-loader cache。
- dynamic import。
- externals。
- webpack-bundle-analyzer。
要分清楚「建置速度」與「使用者載入速度」:
| 方向 | 主要影響 |
|---|---|
| cache / thread-loader | build 速度 |
| splitChunks / dynamic import | 使用者載入與快取 |
| Tree Shaking / minify | 產物體積 |
| CDN / gzip / brotli | 傳輸速度 |
3. happypack 是舊時代方案
有些面試資料會提到 happypack。它是比較早期 webpack 多執行緒建置優化方案。
現在更常見的是:
- webpack 5 cache。
- thread-loader。
- esbuild / swc 類工具。
- Vite + esbuild。
- Rollup chunk 拆分。
面試時可以說:
happypack 偏舊,現在如果是 webpack 5 會優先看持久化 cache、thread-loader 或更快的編譯器;如果是 Vite,則主要看 esbuild 預打包、Rollup 分包與依賴治理。
七、 Source Map 與安全
1. production 要不要開 source map?
答案不是絕對。
開 source map 的好處:
- 線上錯誤能還原到原始碼位置。
- 方便排查 stack trace。
- 搭配 Sentry 等平台更好定位問題。
風險:
- 可能暴露原始碼。
- 產物與上傳流程更複雜。
- 如果公開部署
.map,外部也能讀到更多程式細節。
2. 常見做法:上傳但不公開
成熟做法通常是:
build 產生 source map
-> 上傳到錯誤監控平台
-> 部署靜態資源時不公開 .map這樣線上錯誤平台能還原 stack,但一般使用者不能直接下載 source map。
3. source map 也是效能與安全取捨
source map 不一定會被瀏覽器主動下載,但如果公開存在,仍有安全與資訊暴露問題。
面試時可以說:
production source map 不是不能開,而是要看錯誤追蹤需求與安全策略。常見做法是產生 source map 並上傳到監控平台,部署時不要公開暴露。
八、 追問題庫
Q1:主包 2MB,要怎麼排查?
先用 bundle analyzer 看主包由哪些模組組成。
接著檢查:
- 路由是否懶載入。
- 大型頁面是否進了首屏 bundle。
- UI library 是否全量引入。
- icon 是否全量引入。
- 圖表、地圖、編輯器是否可以延後載入。
- 是否有重複依賴。
- 是否有未使用的 polyfill 或工具庫。
- gzip/brotli 後大小是多少。
不要只說「開 gzip」,因為主包 2MB 往往也代表 parse/execute 成本很高。
Q2:路由懶載入後切頁變慢怎麼辦?
這是分包的典型取捨。
可以考慮:
- 對高機率路由做預取。
- hover 或 idle 時載入下一頁 chunk。
- 把公共依賴抽成穩定 chunk。
- 避免把單一路由拆成太多小 chunk。
- 對重型組件加 loading 狀態。
懶載入減少首屏成本,但可能把成本延到切頁時,必須根據使用者路徑調整。
Q3:CDN 和 gzip 能不能解決 JS 執行慢?
不能完全解決。
CDN 和 gzip/brotli 主要降低下載時間。JS 下載後仍然要解壓、解析、編譯與執行。
如果瓶頸是 main thread long task,就要靠減少 JS、延後初始化、分包、Web Worker 或拆掉重型依賴。
Q4:Tree Shaking 為什麼沒有生效?
可能原因:
- 套件使用 CommonJS。
- import 寫法導致全量引入。
- 套件 side effects 標記不正確。
- 模組本身有頂層副作用。
- 打包工具設定沒有啟用 production 優化。
- 使用的庫不支援良好的 ESM 輸出。
Q5:source map 上線安全嗎?
要看怎麼上線。
如果公開部署 .map 檔,可能暴露原始碼細節。比較好的做法是產生 source map,上傳到錯誤監控平台,部署時不公開。
九、 實作題
題目:Vue SPA 首屏 JS 很大,你怎麼優化?
可以這樣回答:
- 先用 Network 看 JS transfer size、parsed size 與載入順序。
- 用 bundle analyzer 找出最大依賴與主包內容。
- 檢查路由是否懶載入,非首屏頁面不要進主包。
- 檢查重型組件是否可以用
defineAsyncComponent延後載入。 - 檢查 UI library、icon、工具庫是否全量引入。
- 檢查圖表、地圖、編輯器是否能按需載入。
- 檢查是否有重複依賴或不必要 polyfill。
- 設定 gzip/brotli、CDN、hash 檔名與長快取。
- 檢查 source map 是否正確處理,不公開暴露。
- 優化後重新量測 FCP、LCP、JS parse/execute 與主包大小。
這題的重點是:先分析 bundle,再根據依賴和路由拆分處理,不是只背「懶載入、gzip、CDN」。
十、 資深視角
1. 打包優化要看使用者路徑
不是所有程式碼都要最小化到極致。
優先處理:
- 首屏必須下載的資源。
- 高流量路由。
- 轉換關鍵頁。
- 使用者高頻操作功能。
- 低頻但很重的第三方模組。
如果某個功能只有少數人用,就更適合延後載入,而不是進主包。
2. 分包策略要兼顧快取
好的分包不只是檔案變小,也要讓快取穩定。
例如:
- vendor chunk 變動少,可以長快取。
- 業務頁面 chunk 變動較頻繁。
- 首屏 chunk 控制在合理大小。
- 公共依賴不要因為小改動就頻繁改 hash。
但也不要過度迷信 vendor chunk。大型依賴是否應該抽出,要看使用路由、更新頻率和實際載入瀑布圖。
3. 優化要避免把問題轉移
常見副作用:
- 拆包後首屏變快,但切頁變慢。
- 過度 prefetch 搶首屏頻寬。
- 外部 CDN 掛掉影響可用性。
- source map 處理不當暴露原始碼。
- 動態載入太多造成 loading 體驗破碎。
資深回答要能說出:這個方案解決哪個瓶頸,又把成本移到哪裡。
總結
| 問題 | 回答重點 |
|---|---|
| 打包優化解決什麼 | 下載體積、解析執行、快取命中與首屏資源控制 |
| 怎麼減少首屏 JS | 路由懶載入、組件懶載入、第三方庫按需導入 |
| Tree Shaking 關鍵 | ESM、side effects、import 寫法、production build |
| 第三方依賴怎麼治理 | bundle analyzer、重複依賴、重型庫延後載入 |
| 傳輸怎麼優化 | gzip/brotli、CDN、hash 檔名、長快取 |
| source map 怎麼處理 | 可產生並上傳監控平台,但不一定公開部署 |
一句完整的面試回答可以是:
Vue 打包與資源優化我會先用 Network 和 bundle analyzer 找出首屏主包大小、最大依賴和載入瀑布圖。常見做法是路由懶載入、重型組件用 dynamic import 或
defineAsyncComponent延後載入,UI library、icon、工具庫按需導入,避免圖表、地圖、編輯器進入主包。接著會檢查 Tree Shaking 是否生效、是否有重複依賴,再配合 gzip/brotli、CDN、hash 檔名和長快取降低傳輸成本。source map 則依錯誤追蹤需求產生並上傳監控平台,但通常不公開暴露。最後要重新量測首屏、切頁與 JS parse/execute,避免只是把成本從首屏轉移到別的地方。