前端打包工具 (bundler) 是什麼? 為什麼要用?

2025年12月6日

💎 加入 E+ 成長計畫 與超過 800+ 位工程師一同在社群成長,並獲得更多深度的軟體前後端學習資源

前端建構工具 (build tool) 是什麼? 為什麼要用? 一文中,我們談到在現代前端的建構工具中,有打包工具這個重要角色 (如果對這兩個工具的定義不熟,覺得混淆的讀者,推薦回顧建構工具那篇文)。

在前一篇文章中我們有談到,現代前端開發之所以需要建構與打包工具,最主要的點是要平衡開發體驗與程式碼最佳化。

對開發者來說容易維護的程式碼,不代表對機器來說是最佳的。舉例來說,多數 JavaScript 開發團隊會選擇用 TypeScript,透過型別減少錯誤、提高可維護性。但瀏覽器本身無法處理 TypeScript,所以就會需要有工具,把 TypeScript 轉成瀏覽器可處理的 JavaScript。

又或者多數前端團隊不會寫像下面這種程式碼 (這是被最小化後的程式碼),但是從機器的角度來看,這種程式碼不僅功能沒問題,因為體積較小,所需的傳輸時間也會比較短。

function calculatePositiveSum(n) {
  let t = 0;
  for (let r = 0; r < n.length; r++) {
    const c = n[r];
    if (c > 0) t += c;
  }
  return t;
}

由於對開發者與對機器,理想的程式碼長得不一樣,因此我們需要這類工具來協助轉換。在前一篇我們談了建構工具這個大的概念,在這一篇文我們會專注在建構中的打包這個環節,來談打包究竟是什麼、具體怎麼打包 (備註:在資深前端的面試,這問題不算罕見,甚至有公司會在面試時請候選人實作簡單的打包工具)。

JavaScript 的模組演進歷史

在談模組打包工具之前,想先釐清一個詞彙用法,所謂的打包,更完整的說法是模組打包 (module bundling)。要理解這個概念前,讓我們先回顧一下 JavaScript 的模組演進歷史,藉此更具體地理解要被打包的模組,究竟指的是什麼。

在軟體程式中,所謂的模組是指「具有明確邊界、可獨立維護並可重複使用的程式碼」,我們在 寫出好維護的程式碼 透過模組化設計降低軟體複雜度 一文有詳細討論模組設計的概念,推薦讀者回顧。把這個概念拉到 JavaScript 與前端開發來看,當我們在談模組時,就是指可以把類別或函式,拆成不同的檔案,然後透過 exportimport 來重複使用。

然而,exportimport 的語法,雖然在今天通用,但不能被視為理所當然,因為在 JavaScript 剛被發明時,這個語法與機制並不存在。

JavaScript 是個在 10 天內被寫出第一版本的語言,可想而知在最開始有許多東西並不存在於這個語言中,原生的模組化機制也是。在最開始,假如想要把 JavaScript 拆成不同檔案,最簡單的做法就是直接拆,然後在前端的程式碼放入多個 <script> 標籤。

但這樣做有幾個顯而易見的問題。首先,這樣做會讓所有的 <script> 標籤的東西都變成全域的,而全域意味著有衝突的可能。舉例來說,如果兩個 <script> 標籤所引入的 JavaScript 檔案中,有同樣的變數名稱,就會相互干擾。

所以在當時,社群中為了做到能把程式碼模組化,有開發者寫了開源的套件,進而延伸出不同的標準。在 2009 年 Node.js 異軍突起時,因為 Node.js 採用 CommonJS (簡稱 CJS) 這個標準,所以當時 Node.js 相關的專案都是用 module.exports 搭配 require 的方式,來輸出與引用模組 (現在許多有一定年紀的 Node.js 程式碼庫,可能還看得到這種語法)。

然而 CommonJS 的語法除了瀏覽器不支援,因為是同步載入的,所以如果用 require 引入,就會導致瀏覽器必須等到載好該模組後,才會繼續往下處理其他任務,這將導致如果引入的東西多,瀏覽器就會被長時間卡住,相當不理想。所以當時瀏覽器端的開發者,更多會用 Asynchronous Module Definition (簡稱 AMD ) 這個標準。

直到 2015 年 ES6 中,JavaScript 才有了原生支援的模組系統,而這個原生支援的又被稱為 ES Modules (簡稱 ESM),也就是現在多數人熟知的 exportimport

透過這樣的方式,今天假如想要使用某個第三方套件,例如很常會被用的 lodashdebounce 函式,就可以直接 import debounce from 'lodash',不需要自己重新造輪子。

打包 (bundling) 是什麼? 為什麼打包很重要?

上一段落我們談了 JavaScript 當中的模組系統,相信這時讀者會問,有了 ESM 後,為什麼還需要打包呢? 要回答這問題,得先理解什麼是打包,或者說什麼是「模組打包」。

所謂的模組打包,就是把不同的模組包在一起;換句話說,是把多個 JavaScript 檔案,包成數量比較少的 JavaScript 檔案。

以上一段提到的,假如在某個網頁應用的程式碼中,有引入 lodash 這個常見的效用函式庫。例如在程式碼最上方有寫 import debounce from 'lodash',雖然對開發者來說,不需要自己去寫 debounce 這個函式,但對於瀏覽器在跑程式碼來說,還是需要實際有那一段程式碼。

這時問題就來了,瀏覽器不會直接去 npm 下載 lodash 這個套件,那要怎麼拿到那段程式碼來跑?

打包就是在解決這個問題。假如今天使用者進到的頁面,除了開發者自己寫的程式碼外,還用了 lodash 這個套件,這時打包工具會把 lodash 中的程式碼,跟開發者的程式碼包成同一個模組 (例如最後打包出 bundle.js 這個帶有原始程式碼,加上 lodash 程式碼的最終產物),這樣瀏覽器在讀取 bundle.js 時,就也會有 lodash 的程式碼在其中。

為什麼打包很重要?

但為什麼要把不同的模組包在一起?

模組化讓開發體驗比較好 (因為可以輕鬆地 import 要用的東西,以及避免全域命名衝突),但從技術的角度來看,其實也可以完全不需要打包。舉例來說,假如要用 lodash,可以先下載到專案中,然後瀏覽器要的時候,可以再透過 ./node_modules/lodash 之類的方式引用到 <script> 當中。

既然這樣也行,為什麼還要把多個檔案,包成比較少數量的檔案呢?

這是因為從效能的角度來看,發送一個 HTTP 請求來拿模組包,會比發送多個來的快 (HTTP 請求有各類成本,例如 TCP 三次握手、TLS 的連線等等)。進一步說,即使現在 HTTP/2 可以同時發多個請求,仍是有 25 個平行請求的上限,所以如果模組的數量超過 25 個,勢必要把多個整合成一個模組,進而減少請求數量,壓低到 25 個以內。

先前在社群中,甚至有一個演講《How Your Bundle Size Affects The Climate》在談打包產物對氣候的影響。演講中談到,在古早時代的網頁,基本上都是靜態網站不太需要 JavaScript,但到了今天,多數的網站不是靜態的,而是動態的應用程式,所以需要的 JavaScript 程式碼,遠比過往來得多。

當傳輸的 JavaScript 越多,就代表傳輸造成的碳足跡越多,同時瀏覽器要處理的時間也會越多,這中間多做的運算處理,都是額外的耗電,對地球生態帶來負面影響。試想,假如一個每月有千萬人次造訪的網頁應用,每次使用者訪問時所需傳輸的 JavaScript 都能透過打包最小化,不僅會帶給使用者更快速的使用體驗,也對環境更有益,可說是一舉兩得。

閱讀更多

如果你對前端打包工具的主題感興趣,我們在 E+ 當中有更完整的版本,包含會討論社群中熱門的打包工具、如何實作打包。除此之外,E+ 也有更多跟前端工具鏈相關的主題文。

本文為 E+ 成長計畫的深度內容,截取前三分之一開放免費閱讀。歡迎加入 E+ 成長計畫閱讀完整版本 (點此了解 E+ 的詳細介紹)

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們