常見問題
這是關於 esbuild 的常見問題彙整。您也可以在 GitHub 問題追蹤器 上提問。
#為什麼 esbuild 如此快速?
原因有幾個
它是用 Go 編寫的,並編譯為原生程式碼。
大多數其他套件管理程式都是用 JavaScript 編寫的,但對於 JIT 編譯語言來說,命令列應用程式是最差的效能情況。每次執行套件管理程式時,JavaScript VM 都會首次看到套件管理程式的程式碼,而沒有任何最佳化提示。當 esbuild 忙於解析您的 JavaScript 時,node 忙於解析套件管理程式的 JavaScript。當 node 完成解析套件管理程式的程式碼時,esbuild 可能已經結束,而您的套件管理程式甚至尚未開始套件化。
此外,Go 是從核心設計為平行運算,而 JavaScript 則不是。Go 在執行緒之間共用記憶體,而 JavaScript 必須在執行緒之間序列化資料。Go 和 JavaScript 都具有平行垃圾收集器,但 Go 的堆積在所有執行緒之間共用,而 JavaScript 則每個 JavaScript 執行緒有獨立的堆積。根據我的測試,這似乎將 JavaScript 工作執行緒中可能的平行運算量減半 ,這可能是因為一半的 CPU 核心忙於為另一半收集垃圾。
大量使用平行處理。
esbuild 內部的演算法經過仔細設計,在可能的情況下充分飽和所有可用的 CPU 核心。大致分為三個階段:解析、連結和程式碼產生。解析和程式碼產生是大部分的工作,且可完全平行化(連結在大部分情況下本質上是串列任務)。由於所有執行緒共用記憶體,因此在綑綁匯入相同 JavaScript 函式庫的不同進入點時,可以輕鬆地共用工作。大多數現代電腦都有許多核心,因此平行處理是一大優勢。
esbuild 中的所有內容都是從頭開始編寫的。
與其使用第三方函式庫,不如自己編寫所有內容,這樣可以帶來許多效能優勢。您可以從一開始就考慮效能,您可以確保所有內容都使用一致的資料結構以避免昂貴的轉換,並且您可以在必要時進行廣泛的架構變更。當然,缺點是工作量很大。
例如,許多綑綁器使用官方 TypeScript 編譯器作為解析器。但它是為了服務 TypeScript 編譯器團隊的目標而建構的,而他們並未將效能視為首要考量。他們的程式碼大量使用 巨型物件形狀 和不必要的 動態屬性存取(這兩個都是著名的 JavaScript 速度障礙)。而且 TypeScript 解析器似乎即使在停用類型檢查的情況下,仍然會執行類型檢查器。這些都不是 esbuild 自訂 TypeScript 解析器會遇到的問題。
有效率地使用記憶體。
理想情況下,編譯器在輸入長度上的複雜度主要是 O(n)。因此,如果您要處理大量資料,則記憶體存取速度可能會嚴重影響效能。您必須對資料進行的遍歷次數越少(以及您需要將資料轉換為的不同表示形式越少),您的編譯器執行速度就會越快。
例如,esbuild 只會觸及整個 JavaScript AST 三次
- 詞法分析、解析、範圍設定和宣告符號的遍歷
- 繫結符號、縮小語法、JSX/TS 轉換為 JS 和 ESNext 轉換為 ES2015 的遍歷
- 縮小識別碼、縮小空白、產生程式碼和產生原始碼對應的遍歷
這會在 AST 資料仍然在 CPU 快取中時,最大化其重複使用。其他綑綁器會在個別遍歷中執行這些步驟,而不是將它們交錯進行。它們也可能會在資料表示形式之間進行轉換,以將多個函式庫黏合在一起(例如字串→TS→JS→字串,然後字串→JS→較舊的 JS→字串,然後字串→JS→縮小的 JS→字串),這會使用更多記憶體並降低速度。
Go 的另一個優點是它可以將資料緊湊地儲存在記憶體中,這使它能夠使用更少的記憶體並在 CPU 快取中容納更多資料。所有物件欄位都有類型,而且欄位緊密地封裝在一起,因此例如幾個布林旗標各自只佔用一個位元組。Go 也具有值語意,並且可以將一個物件直接嵌入另一個物件中,因此它可以「免費」獲得,而不需要進行另一個配置。JavaScript 沒有這些功能,而且還有其他缺點,例如 JIT 負擔(例如隱藏類別槽)和效率低下的表示形式(例如非整數數字會使用指標堆積配置)。
這些因素每一個都只會帶來一些加速,但加起來它們可以讓打包器比現今其他常用的打包器快上好幾個數量級。
#基準測試詳細資訊
以下是每個基準測試的詳細資訊
此基準測試透過將 three.js 函式庫複製 10 次,並從頭開始建立一個單一程式包,且不使用任何快取,來近似一個大型 JavaScript 程式碼庫。可以在 esbuild repo 中使用 make bench-three
來執行基準測試。
打包器 | 時間 | 相對減速 | 絕對速度 | 輸出大小 |
---|---|---|---|---|
esbuild | 0.39 秒 | 1 倍 | 1403.7 kloc/s | 5.80mb |
parcel 2 | 14.91 秒 | 38 倍 | 36.7 kloc/s | 5.78mb |
rollup 4 + terser | 34.10 秒 | 87 倍 | 16.1 kloc/s | 5.82mb |
webpack 5 | 41.21 秒 | 106 倍 | 13.3 kloc/s | 5.84mb |
每個報告的時間都是三趟執行中最好的。我使用 --bundle
來執行 esbuild。我使用 @rollup/
外掛程式,因為 Rollup 本身不支援壓縮。Webpack 5 使用 --mode=
。Parcel 2 使用預設選項。絕對速度是根據總行數計算,包括註解和空白行,目前為 547,441。這些測試是在配備 16gb RAM 的 6 核 2019 MacBook Pro 上執行,並已停用 macOS Spotlight。
此基準測試使用舊的 Rome 程式碼庫(在他們重新用 Rust 編寫之前)來近似一個大型 TypeScript 程式碼庫。所有程式碼都必須合併成一個單一的壓縮程式包,並附有原始碼對應表,而且產生的程式包必須能正常運作。可以在 esbuild repo 中使用 make bench-rome
來執行基準測試。
打包器 | 時間 | 相對減速 | 絕對速度 | 輸出大小 |
---|---|---|---|---|
esbuild | 0.10 秒 | 1 倍 | 1318.4 kloc/s | 0.97mb |
parcel 2 | 6.91ѕ | 69 倍 | 16.1 kloc/s | 0.96mb |
webpack 5 | 16.69ѕ | 167 倍 | 8.3 kloc/s | 1.27mb |
每個報告的時間都是三趟執行中最好的。我使用 --bundle
來執行 esbuild。Webpack 5 使用 ts-loader
,並設定 transpileOnly:
和 --mode=
。Parcel 2 在 package.json
中使用 "engines":
。絕對速度是根據總行數計算,包括註解和空白行,目前為 131,836。這些測試是在配備 16gb RAM 的 6 核 2019 MacBook Pro 上執行,並已停用 macOS Spotlight。
結果不包含 Rollup,因為我無法讓它運作,原因與 TypeScript 編譯有關。我嘗試了 @rollup/
,但無法停用類型檢查,我也嘗試了 @rollup/
,但沒有辦法提供 tsconfig.json
檔案(這是正確路徑解析所需要的)。
#即將推出的路線圖
這些功能已經在進行中,並且是首要優先事項
這些是潛在的未來功能,但可能不會發生,或可能在更有限的範圍內發生
- HTML 內容類型 (#31)
在那之後,我會認為 esbuild 相對完整。我計畫讓 esbuild 達到一個大致穩定的狀態,然後停止累積更多功能。這將涉及對在 esbuild 本身中加入主要功能的要求說「不」。我不認為 esbuild 應該成為所有前端需求的一體化解決方案。特別是,我想避免「webpack 設定」模型的痛苦和問題,其中底層工具太過靈活,而可用性受到影響。
例如,我不計畫在 esbuild 的核心本身加入這些功能
我希望我加入 esbuild 的可擴充性點(外掛程式和 API)將使 esbuild 可用於包含在更自訂的建置工作流程中,但我並非預期或打算讓這些可擴充性點涵蓋所有使用案例。如果你有非常自訂的需求,你應該使用其他工具。我也希望 esbuild 激勵其他建置工具透過大規模修改其實作來大幅提升效能,讓所有人都能受益,而不僅僅是使用 esbuild 的人。
我計畫在 esbuild 達到穩定後,繼續維護 esbuild 現有範圍內的所有內容。這表示實作對新發布的 JavaScript 和 TypeScript 語法功能的支援,例如。
#生產就緒
這個專案尚未達到 1.0.0 版本,仍處於積極開發中。話雖如此,它已經遠遠超過 alpha 階段,而且相當穩定。我認為它是一個後期 beta 版。對一些早期採用者來說,這表示它已經足夠好,可以用於實際事物。一些其他人認為這表示 esbuild 尚未準備好。本節不會試圖說服你採取任何一方的立場。它只是試圖提供足夠的資訊,讓你能夠自行決定是否要使用 esbuild 作為你的套件管理工具。
一些數據點
- 其他專案使用
此 API 已作為許多其他開發人員工具中的函式庫使用。例如,Vite 和 Snowpack 使用 esbuild 將 TypeScript 轉換為 JavaScript,而 Amazon CDK(雲端開發套件)和 Phoenix 使用 esbuild 來打包程式碼。
- API 穩定性
儘管 esbuild 的版本尚未達到 1.0.0,但仍致力於保持 API 的穩定性。修補程式版本適用於向下相容的變更,而次要版本則適用於向下不相容的變更。如果您計畫將 esbuild 用於實際用途,您應該釘選確切版本(最大安全性)或釘選主要和次要版本(僅接受向下相容的升級)。
- 僅有一位主要開發人員
此工具主要是由我建置的。對某些人來說,這沒問題,但對其他人來說,這表示 esbuild 並非適合其組織的工具。我對此沒意見。我建置 esbuild,是因為我覺得建置很有趣,而且這是我想使用的工具。我與全世界分享,是因為還有其他人也想使用它,因為回饋可以讓工具本身變得更好,而且我相信它會激勵生態系統製作更好的工具。
- 並非總是願意擴充範圍
我沒有計畫納入我不感興趣建置和/或維護的主要功能。我也想限制專案的範圍,讓它不會變得過於複雜且難以控制,無論是從架構觀點、測試和正確性觀點,還是從可用性觀點來看。將 esbuild 視為網路的「連結器」。它知道如何轉換和打包 JavaScript 和 CSS。但是,您的原始程式碼如何最終成為純 JavaScript 或 CSS 的詳細資訊可能需要第三方程式碼。
我希望外掛程式能讓社群新增主要功能(例如 WebAssembly 匯入),而無需貢獻給 esbuild 本身。然而,並非所有內容都公開在外掛程式 API 中,而且可能無法將您可能想新增到 esbuild 的特定功能新增進去。這是故意的;esbuild 並非旨在成為所有前端需求的一體化解決方案。
#防毒軟體
由於 esbuild 是以原生程式碼撰寫,因此防毒軟體有時會錯誤地將其標記為病毒。這並不表示 esbuild 是病毒。我不會發布惡意程式碼,而且我非常重視供應鏈安全性。
esbuild 的程式碼幾乎都是第一方的,只有一個 相依性 是來自 Google 的 Go 套件補充集。我的開發工作是在與我用來發布建置的機器不同的機器上進行的。我已執行其他工作以確保 esbuild 發布的建置完全可重製,且在每次發布後,發布的建置都會 自動與 在不相關環境中本地建置的建置進行比較,以確保它們在位元上相同(即 Go 編譯器本身未遭到破壞)。您也可以自己從原始碼建置 esbuild,並將您的建置成品與發布的成品進行比較,以獨立驗證這一點。
使用防毒軟體處理誤判是無可避免的現實。如果您的防毒軟體不讓您使用 esbuild,以下是可能的解決方法
- 忽略您的防毒軟體,並從隔離中移除 esbuild
- 向您的防毒軟體供應商回報特定 esbuild 原生可執行檔為誤判
- 使用
esbuild-wasm
取代esbuild
,以繞過您的防毒軟體(它可能不會像標記原生可執行檔那樣標記 WebAssembly 檔案) - 使用其他建置工具取代 esbuild
#過時的 Go 版本
如果您使用自動相依性漏洞掃描器,您可能會收到報告,指出 esbuild 使用的 Go 編譯器版本和/或 golang.org/x/sys
(esbuild 唯一的相依性)的版本已過時。這些報告是良性的,應予以忽略。
這是因為 esbuild 的程式碼故意設計為可與 Go 1.13 相容。後來的 Go 版本已不再支援某些較舊的平台,而我希望 esbuild 能夠在這些平台上執行(例如較舊版本的 macOS)。雖然 esbuild 發布的二進位檔是用更新版本的 Go 編譯器編譯的(因此無法在較舊版本的 macOS 上執行),但您目前仍可以使用 Go 1.13 為自己編譯最新版本的 esbuild,並在較舊版本的 macOS 上使用它,因為 esbuild 的程式碼仍然可以與 Go 1.13 及其後續版本相容。
人們和/或自動化工具有時會看到 go.mod
中的 go 1.13
行,並抱怨 esbuild 發布的二進位檔是用 Go 1.13 建置的,而 Go 1.13 是 Go 的一個非常舊的版本。然而,事實並非如此。go.mod
中的那一行只指定最低編譯器版本。它與 esbuild 發布的二進位檔所建置的 Go 版本無關,後者是更新版本的 Go。 請閱讀文件。
人們有時也希望 esbuild 更新 golang.org/x/sys
相依性,因為 esbuild 使用的版本存在已知的漏洞(特別是關於 Faccessat
函數的 GO-2022-0493)。阻止 esbuild 更新到更新版本的 golang.org/x/sys
相依性的問題是,較新的版本已開始使用 unsafe.Slice
函數,該函數最早於 Go 1.17 中引入(因此無法編譯到較舊版本的 Go 中)。然而,這個漏洞報告無關緊要,因為 a) esbuild 根本不會呼叫該函數,而且 b) esbuild 是建置工具,不是沙箱,而 esbuild 的檔案系統存取並非安全性敏感的。
我不會放棄與較舊平台的相容性,並阻止某些人使用 esbuild,只為了解決無關緊要的漏洞報告。請忽略任何關於上述問題的報告。
#縮小的換行符
人們有時會驚訝於 esbuild 的縮小程式通常會將 JavaScript 字串中的字元跳脫序列 \n
變更為範本字串中的換行字元。但這是故意的。這不是 esbuild 的錯誤。縮小程式的任務是產生與輸入等效的輸出,且輸出盡可能精簡。字元跳脫序列 \n
長度為兩個位元組,而換行字元長度為一個位元組。
例如,這個程式碼長度為 21 個位元組
var text="a\nb\nc\n";
而這個程式碼長度為 18 個位元組
var text=`a
b
c
`;
因此,第二個程式碼已完全縮小,而第一個程式碼則沒有。縮小程式碼並不表示將所有程式碼放在一行上。相反地,縮小程式碼表示產生等效的程式碼,且使用最少的位元組。在 JavaScript 中,未標記的範本字串等同於字串字串,因此 esbuild 在這裡執行的動作是正確的。
#避免名稱衝突
在瀏覽器中執行 esbuild 輸出時,進入點模組中的頂層變數不應出現在全域範圍。如果發生這種情況,表示您未遵循 esbuild 關於輸出格式的文件,並錯誤地使用 esbuild。這不是 esbuild 的錯誤。
特別是,在瀏覽器中執行 esbuild 輸出時,您必須執行下列其中一項動作
--format=
搭配iife <script
src="..."> 如果您在全域範圍執行程式碼,則應使用
--format=
。這會讓 esbuild 的輸出包裝您的程式碼,以便在巢狀範圍中宣告頂層變數。iife --format=
搭配esm <script
src="..." type="module"> 如果您使用
--format=
,則必須以模組執行您的程式碼。這會導致瀏覽器封裝您的程式碼,以便在巢狀範圍中宣告頂層變數。esm
將 --format=
與 <script
搭配使用會以微妙且令人困惑的方式中斷您的程式碼(省略 type="
表示所有頂層變數最終都會出現在全域範圍中,然後會與其他 JavaScript 檔案中具有相同名稱的頂層變數發生衝突)。
#頂層 var
人們有時會驚訝於 esbuild 有時會將頂層 let
、const
和 class
宣告改寫為 var
宣告。這是出於以下幾個原因
- 為了正確性
打包有時需要延遲初始化模組。例如,當您使用套件中的模組路徑呼叫
require()
或import()
時,就會發生這種情況。這樣做涉及透過將初始化移至封閉函式中來分離頂層符號的宣告和初始化。因此,例如class
陳述式會改寫為將類別運算式指定給變數。將宣告保留在延遲初始化封閉函式之外對於效能非常重要,因為這表示其他模組可以直接透過名稱而不是透過較慢的屬性存取間接地參照它們。需要這樣做的另一個情況是轉換頂層
using
宣告。這涉及將整個模組主體包覆在try
區塊中,其中也包括分離頂層符號的宣告和初始化。頂層符號可能需要匯出,這表示它們不能在try
區塊中宣告。在這兩種情況下,如果原始碼包含
const
符號的突變,esbuild 會傳回建置錯誤,因此 esbuild 將頂層const
改寫為var
不可能導致常數的突變。由於 esbuild 目前的架構,執行此轉換的部分(解析器)無法知道目前的模組最終是否會延遲初始化。此決策的資訊可能只會在建置的稍後階段發現,甚至可能在重複使用相同 AST 的未來增量建置中變更(每個檔案的 AST 在解析期間轉換一次,然後快取並在增量建置中重複使用)。因此,當打包處於啟用狀態時,總是會執行此轉換。
- 為了效能
多個 JavaScript VM 已經且持續出現 TDZ(即「時間死區」)檢查的效能問題。這些檢查驗證 let、const 或 class 符號是否在初始化之前使用。以下是兩個知名 VM 的兩個問題
- V8: https://bugs.chromium.org/p/v8/issues/detail?id=13723(10% 速度變慢)
- JavaScriptCore: https://bugs.webkit.org/show_bug.cgi?id=199866(1,000% 速度變慢!)
JavaScriptCore 有一個嚴重的效能問題,因為他們的 TDZ 實作時間複雜度是需要在同一個範圍內進行 TDZ 檢查的變數數量的平方(通常最糟糕的狀況是頂層範圍)。V8 有持續的問題,即使在同一個函式中已經檢查過,或是相關函式已經執行過(因此檢查已經進行過),TDZ 檢查仍然存在於 JIT 所產生的程式碼中。
在 JavaScript 中,
let
、const
和class
宣告都會引入 TDZ 檢查,而var
宣告則不會。由於套件通常會將許多模組合併成一個非常大的頂層範圍,因此這些 TDZ 檢查的效能影響可能會非常嚴重。將頂層的let
、const
和class
宣告轉換成var
有助於自動讓您的程式碼執行得更快。
請注意,esbuild 沒有保留頂層 TDZ 的副作用,因為模組可能需要延遲初始化(如上所述),這表示要將宣告與初始化分開。頂層符號的 TDZ 檢查理論上仍然可以透過產生額外的程式碼來支援,該程式碼會在每次使用頂層符號之前進行檢查,如果尚未初始化,則會擲回例外(有效地手動實作真實 JavaScript VM 會執行的動作)。然而,這對於程式碼大小和執行時間來說似乎都是過度的負擔,而且似乎不是生產導向套件應該做的事情。