新舊時代 JS Bundler 的世代交替 - Vite vs. Webpack 的詳細比較
在過去,當我們談論到 JavaScript 前端開發環境時,很難不提到 Webpack
。這款在 2012 年誕生的強大工具,在過去的 10 年內一直是最主流的前端打包工具 。然而,在 2020 年一個名為 Vite
的新興工具迅速崛起,挑戰著 Webpack 的霸主地位。根據 2023 年 State of JavaScript 網站所統計的資料顯示,Vite 僅花了短短三年就成為使用規模第二大的 Build Tools,如果單看 Interest 或 Positivity 指標的話,甚至都穩坐第一名的位置。
持續關注 JS 前端生態圈的讀者一定有感受到,近兩年 Vite 人氣竄升的速度之快,在 Vite 快速竄紅的一段時間裡甚至瀰漫著「Webpack 已死」的氛圍。由於我目前的公司所使用的工具是 Webpack,而我自己開專案練習時則會選擇 Vite,因此我能明白兩者之間易用性的差距,以及為什麼現在的前端開發者越來越偏好使用 Vite。但撇除像是開箱及用、快速的 HMR 等那些 Vite 被廣為人知的優點外,我還是想要知道兩者在更深層次的技術細節上有何不同。這篇文章整理了我對 Vite 與 Webpack 兩者如何提供現代 Bundler 所需具備的功能的理解,並探討它們各自的適用場景和潛在限制。
本篇文章內容整理自 Vite vs. Webpack: A Head-to-Head Comparison 與 Vite 官方網站
何謂 Bundler?
在探討 Vite 與 Webpack 這兩個工具包山包海的的功能之前,我們得先了解這兩個工具在 Web 應用中所代表的角色:Bundler
Bundler 的定義與功能
JavaScript Module Bundler 是一種用於網頁開發的工具,旨在將多個 JavaScript 檔案合併成一個或少數幾個檔案,這個合併檔案稱為 bundle 。這樣的合併過程可以減少網頁應用程式所需的請求數量,從而提升效能和加快載入速度。
實例
假設我們有兩個獨立的 JavaScript 檔案:module1.js 和 module2.js。
// module1.js
export const greet = (name) => {
console.log(`Hello, ${name}!`);
}
// module2.js
export const farewell = (name) => {
console.log(`Goodbye, ${name}!`);
}
透過使用 JavaScript bundler(如 Rollup、Webpack 或 Parcel),我們可以將這些模組與一個 index.js 檔案合併:
// index.js
import { greet } from './module1.js';
import { farewell } from './module2.js';
greet('Kinsta');
farewell('Server Troubles');
Bundler 會將 module1.js、module2.js 和 index.js 合併為一個最佳化後的 bundle,方便在網頁應用程式中使用。
優點
- 減少 HTTP 請求:合併多個檔案,減少請求數量。
- 程式碼最佳化:進行程式碼壓縮、混淆等,減少檔案大小。
- 跨瀏覽器相容性:解決瀏覽器特定問題,提供一致的使用者體驗。
- 程式碼轉換:進行必要的程式碼轉換,如轉譯(transpilation),以確保在所有目標環境中運行。
儘管現代瀏覽器支援 ES modules 及 HTTP/2 技術,這些技術能解決請求開銷問題,但 JavaScript bundlers 依然不可或缺,因為它們可以執行關鍵的程式碼轉換和最佳化。
簡單認識 Vite 與 Webpack
Vite - 最佳開發體驗的現代化工具
Vite 是一個現代化的前端建構工具,旨在提供快速的開發體驗和高效的建構效能。由於它使用了原生 ES 模組和 Rollup 作為底層技術,Vite 可以在開發階段實現快速的熱更新(Hot Module Replacement,HMR),並在生產環境中進行高效的程式碼打包。
Vite 的特色
- 快速冷啟動:利用瀏覽器的原生 ES 模組支援,Vite 只需啟動必要的部分,避免了打包整個應用程式的時間。
- 即時模組熱更新:Vite 的 HMR 機制能夠在程式碼改變時快速更新頁面,提升開發效率。
- 現代化語法支援:內建支援 TypeScript、JSX 和 CSS 前置處理器(如 Sass、Less)。
- 輕量且易於配置:相較於傳統的打包工具,Vite 的配置更加簡單直觀,減少了學習成本。
- 高度擴展性:基於外掛系統,可以輕鬆擴展 Vite 的功能,以滿足不同專案需求。
Webpack - 全能的模組打包工具
Webpack 是一個功能強大的模組打包工具,曾經是前端開發的主流選擇。它能夠處理多種資源類型(如 JavaScript、CSS、影像等),並提供豐富的配置選項和外掛系統。
Webpack 的特色
- 靈活性和擴展性:Webpack 的配置文件支援多種選項和外掛,能夠處理各種複雜的應用場景。
- 模組化管理:支援多種模組格式(如 CommonJS、AMD、ES Modules),有效管理程式碼相依性。
- 強大的社群支援:擁有廣泛的使用者基礎和豐富的第三方外掛資源。
- 豐富的功能:提供從開發到生產環境的全面支援,包括熱模組替換、程式碼壓縮、程式碼分割(code split)、懶載入(lazy loading)和混淆等。
架構與設計哲學
Vite 的設計哲學
Vite 的設計哲學圍繞輕量化和可擴展性,強調簡潔和長期的專案可維護性。Vite 的核心架構分為開發階段和生產建構階段,透過利用現代瀏覽器的原生對 ES 模組的支持和快速的模組熱更新(HMR)機制,提升開發效率。
-
開發階段:Vite 利用瀏覽器的 原生 ES 模組支援,只需啟動必要部分,降低整個應用程式的冷啟動時間。當程式碼改變時,Vite 只重新編譯改動部分,並通過模組熱更新(HMR)快速反映在瀏覽器中,極大提升開發效率。
-
生產建構階段:在建構生產環境程式碼時,Vite 使用 Rollup 進行打包。Rollup 擅長生成高效的程式碼,並提供豐富的外掛系統,可以對程式碼進行各種最佳化,如:程式碼分割、壓縮、混淆等。
-
外掛系統:Vite 的外掛系統基於 Rollup,擴展性強。開發者可以輕鬆地新增或自訂外掛,以滿足特定的開發需求,防止核心臃腫。Vite 積極與 Rollup 專案合作,保持相容性和共享外掛生態系統。
-
設計實踐:Vite 的設計哲學體現在它對現代 JavaScript 特性的支援上,如 ES 模組和 Worker 語法。例如:
// app.js
import { greet } from './utilities.js';
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage({ input: 42 });
worker.onmessage = (e) => {
const result = e.data.result;
console.log(`Result from the web worker: ${result}`);
};
const message = greet('Hello, Vite!');
console.log(message);這段程式碼展示了 Vite 如何無縫支援 ES 模組和 Worker 語法。在這個例子中,我們使用 ES 模組語法從 utilities.js 模組中匯入 greet 函數,並且通過 new URL('./worker.js', import.meta.url) 建立了一個新的 Web Worker,這在 Vite 中得到原生支援而不需要額外配置。這樣的設計大大簡化了開發過程,使得開發者可以更專注於功能實現而非工具配置。
Webpack 的設計哲學
Webpack 的 設計哲學是以模組化為核心,通過強大的配置和外掛系統,實現高度靈活的打包和資源管理。Webpack 被設計為一個功能齊全的模組打包工具,適用於各種複雜的應用場景。
-
模組化管理:Webpack 支援多種模組格式(如 ES2015 的 import 語句、CommonJS 的 require 語句、AMD 的 define 和 require 語句,以及 CSS/Sass/Less 文件中的 @import 語句等),通過 Loader 將它們轉換為可以在 JavaScript 中引用的模組。這種模組化的處理方式類似於使用樂高積木搭建專案,提升程式碼的可讀性和維護性。
-
配置靈活:Webpack 提供了高度可配置的選項,開發者可以通過配置文件詳細定義打包過程中的各種行為,如入口文件、輸出文件、模組解析規則等。
-
外掛系統:Webpack 擁有豐富的外掛系統,幾乎所有的打包過程都可以通過外掛進行擴展和自訂,這使得 Webpack 能夠滿足各種需求,保持靈活性和可擴展性。
-
程式碼最佳化:Webpack 提供了多種程式碼最佳化技術,如程式碼分割、Tree Shaking、程式碼壓縮和混淆,這些技術有助於生成高效的生產環境程式碼。
-
設計實踐:Webpack 的模組化設計特別適合大型專案。透過配置和使用 loader,開發者可以將專案組織成模組,提高可讀性和維護性。例如:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './app.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
};這個例子展示了 Webpack 如何配置建構過程,最佳化程式碼並高效處理資源,通過使用 Babel Loader 寫出清晰、模組化的程式碼,提高使用者體驗。
Vite 與 Webpack 的共通點
儘管 Vite 和 Webpack 在設計哲學上有所不同,也存在一些共通點:
-
模組化設計: 兩者都採用模組化設計哲學,鼓勵將應用程式拆分成小的、可重用的模組。這種設計方式有助於提高程式碼的可維護性和可擴展性。
-
外掛驅動架構: Vite 和 Webpack 都使用外掛驅動的架構。這種架構允許開發者根據需求擴展工具的功能,而不需要改變其核心程式碼。外掛機制提供了極大的靈活性,讓開發者可以自訂打包流程。
配置和易用性
精簡的 Vite 配置
Vite 的設計目標之一是提供簡單易用的配置,使開發者能夠快速上手並專注於開發工作。Vite 通常需要極少的配置或幾乎不需要配置,這是其「開箱即用」的關鍵所在。
-
默認配置即用:Vite 提供了合理的默認配置,涵蓋了大多數常見的開發需求。開發者無需進行大量配置即可啟動和運行項目。
-
簡單的配置文件:如果需要自定義配置,可以通過在項目根目錄下創建
vite.config.js
文件來完成。配置文件結構簡單直觀,例如:在上述例子中,我們僅導入並安裝了 Vite 的 Vue.js 官方插件。Vite 的優勢在於其自動檢測大多數項目的正確設置,減少配置的麻煩。// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
}); -
插件系統:Vite 的插件系統基於 Rollup,使得配置和擴展變得簡單。只需安裝並引用相應的插件,即可輕鬆添加所需功能。
-
開發伺服器配置:Vite 的開發伺服器配置也非常簡單,可以在
vite.config.js
中進行設置,如:export default defineConfig({
server: {
port: 3000,
},
});
複雜的 Webpack 配置
相比之下,Webpack 的配置更為複雜和靈活,這是由於其強大的功能和廣泛的應用場景所決定的。雖然 Webpack 在最近的版本中也朝向零配置方向發展,但仍然不如 Vite 自動化。Webpack 可以處理各種不同的需求,但相應的配置也變得更加繁瑣和詳細。
-
詳細的配置文件:Webpack 的配置文件結構複雜,涵蓋了入口點、輸出、模組解析規則、插件等多個方面。例如:這段配置文件展示了 Webpack 如何配置構建過程,優化代碼並高效處理資源。相比 Vite,Webpack 的配置涉及更多手動設置,包括指定入口和輸出路徑、配置 Loader 處理不同類型的文件、設置插件以實現特定功能。
// webpack.config.js
const webpack = require('webpack');
const path = require('path');
const { HotModuleReplacementPlugin } = require('webpack');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
},
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader'],
},
],
},
resolve: {
alias: {
vue: 'vue/dist/vue.js',
},
},
plugins: [
new HotModuleReplacementPlugin(),
new VueLoaderPlugin(),
],
}; -
豐富的插件和 Loader:Webpack 擁有大量的插件和 Loader,可以滿足各種需求,但這也增加了配置的複雜性。開發者需要根據具體需求選擇和配置合適的插件和 Loader。
-
靈活的自定義:Webpack 允許開發者自定義構建流程的各個方面,這意味著開發者需要對配置有深入的理解,以充分利用其強大的功能。
-
多配置文件:對於大型項目,通常需要多個配置文件來處理開發、測試和生產環境,這進一步增加了配置的複雜性。
開發端伺服器
伺服器配置
Vite 和 Webpack 都提供了強大的開發伺服器功能,但在配置和使用上有所不同,對開發效率和生產力有直接影響。
-
Vite 的伺服器配置:Vite 的開發伺服器是內建的,開箱即用,通常不需要額外配置。只需在
vite.config.js
中進行少量設定即可。例如:// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
open: true, // 啟動伺服器時自動打開瀏覽器
proxy: {
'/api': 'http://localhost:5000',
},
},
}); -
Webpack 的伺服器配置:Webpack 提供了多種選項,如
webpack-dev-server
和webpack-dev-middleware
,需要額外設定才能自動編譯程式碼並處理變更。配置相對複雜,例如:// webpack.config.js
const webpack = require('webpack');
const path = require('path');
const { HotModuleReplacementPlugin } = require('webpack');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
},
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader'],
},
],
},
resolve: {
alias: {
vue: 'vue/dist/vue.js',
},
},
plugins: [
new HotModuleReplacementPlugin(),
new VueLoaderPlugin(),
],
devServer: {
contentBase: path.join(__dirname, 'public'),
port: 3000,
hot: true,
open: true,
proxy: {
'/api': 'http://localhost:5000',
},
},
};
冷啟動速度
- Vite 的冷啟動速度:
Vite 通過使用 esbuild(基於 Go 的高性能打包工具)來預打包依賴項,大幅減少初始化時間。Vite 的設計利用原生 ES 模組,只在瀏覽器請求時進行程式碼轉換和服務,從而顯著提高了伺服器啟動速度。
- 高效的依賴處理:Vite 預打包依賴項,例如將
lodash-es
的 600 多個內部模組合併為一個模組,減少 HTTP 請求數量,提升開發伺服器性能。 - 按需載入:Vite 使用原生 ES 模組按需載入原始碼,最小化伺服器負載和延遲。
- 高效的依賴處理:Vite 預打包依賴項,例如將
- Webpack 的冷啟動速度:
Webpack 傾向於在啟動時預打包原始碼和依賴項,這種捆綁方法在開發過程中延長了伺服器啟動時間。相比之下,Webpack 的伺服器設定時間通常比 Vite 更長。
- bundle 方法:Webpack 的 bundle 方法確保所有站點資料都可用,使開發伺服器中的頁面導航速度更快,但初始化時間較長。
HMR (Hot Module Replacement)
- Vite 的 HMR:Vite 使用原生 ESM 實現 HMR,通過將部分捆綁工作轉移到瀏覽器,減少伺服器負載和延遲。這確保了快速更新而無需整頁重新載入,對開發過程中的即時反饋至關重要。
- Webpack 的 HMR:Webpack 也支援 HMR,實現即時更新並保留開發過程中的應用狀態。與 Vite 不同,Webpack 需要在配置文件中啟用相關外掛和設定。
快取效能
1. Vite 的快取效能:
- 瀏覽器原生快取:Vite 利用瀏覽器對 ES 模組的原生支援,通過 HTTP 提供模組,瀏覽器能夠自行快取這些模組,避免了重複加載,提高了開發環境中的響應速度。
- 依賴預構建快取:Vite 會在第一次啟動時預構建並快取第三方依賴 (如 npm 包),這些依賴會被儲存在本地快取中,隨後的啟動只需要重用這些預構建的快取,減少了重新解析和打包的時間。
- 模組熱替換 (HMR) 快取:在開發過程中,Vite 的 HMR 只會更新實際變動的模組,而不會重新加載整個應用程式。這種精細的快取策略確保了更快的開發迭代速度。
2. Webpack 的快取效能:
- 持久快取:Webpack 支持持久化快取(Persistent Caching),可以將編譯過程中生成的模組和資產緩存在磁碟中。在隨後的構建中,Webpack 可以重用這些快取,顯著減少了編譯時間。
- 模組快取 (Module Caching):Webpack 在開發過程中會將已編譯的模組快取起來,當模組未發生變化時,會直接使用快取的結果,而不是重新編譯,從而提高建構速度。
- DLL 外掛 (DLL Plugin):Webpack 提供了 DLL 外掛,用於提前打包和快取不常變化的第三方庫。這些庫可以在開發和建構過程中重複使用,減少重複編譯的開銷。
- 最佳化輸出快取 (Output Caching):Webpack 可以根據文件內容生成文件名的 hash 值,從而實現瀏覽器層面的快取。當文件內容未變化時,文件名不變,瀏覽器可以直接使用快取的文件,提升使用者的載入體驗。
最佳化打包流程
預載指令生成
預載指令(Preload Directives)是用於告訴瀏覽器哪些資源應該優先載入的技術,以加速網頁的首次渲染。
-
Vite 的預載指令生成: Vite 自動在構建的 HTML 中為入口塊(entry chunks)生成
<link rel="modulepreload">
指令,並直接匯入他們,從而提高載入速度。例如:<!-- Vite - Module Preloading -->
<link rel="modulepreload" href="/module-a.js"> -
Webpack 的預載指令生成: Webpack 最初並不原生支持資源提示(Resource Hints),但從 v4.6.0 開始,它增加了對預加載(Prefetching)和預載(Preloading)的支持。通過在導入模組時使用內嵌指令,Webpack 可以輸出資源提示,告訴瀏覽器何時加載要導入的模組文件。例如:
import(/* webpackPreload: true */ '/module-a.js');
這會輸出:
<!-- Webpack - Manual Module Preloading -->
<link rel="preload" as="script" href="/module-a.js">
CSS Code Splitting
CSS Code Splitting 是將 CSS 程式碼拆分為多個文件,以便按需載入,提高頁面載入性能。
-
Vite 的 CSS Code Splitting:
Vite 能夠自動提取異步塊(async chunks)中的 CSS 並生成單獨的文件,這意味著當加載相關的異步塊時,只有必要的 CSS 會通過
<link>
標籤被加載。此外, Vite 會確保在 CSS 加載完成後才開始解析異步塊避免了 Flash of Unstyled Content(FOUC)的問題。