esbuild
是新一代的 JavaScript 打包工具。前端
他的做者是 Figma
的 CTO - Evan Wallace
。node
esbuild
以速度快
而著稱,耗時只有 webpack 的 2% ~3%。webpack
esbuild
項目主要目標是: 開闢一個構建工具性能的新時代,建立一個易用的現代打包器
。git
它的主要功能:github
Extreme speed without needing a cache
ES6 and CommonJS modules
Tree shaking of ES6 modules
An API for JavaScript and Go
TypeScript and JSX syntax
Source maps
Minification
Plugins
如今不少工具都內置了它,好比咱們熟知的:web
vite
,snowpack
藉助 esbuild 優異的性能, vite 更是如虎添翼, 快到飛起。算法
今天咱們就來探索一下: 爲何 esbuild 這麼快。緩存
今天的主要內容:服務器
幾組性能數據對比
爲何 esbuild 這麼快
esbuild upcoming roadmap
esbuild 在 vite 中的運用
爲何生產環境仍需打包?
爲什麼vite不用 esbuild 打包?
總結
先看一組對比:網絡
使用 10 份 threeJS 的生產包,對比不一樣打包工具在默認配置下的打包速度。
webpack5 墊底, 耗時 55.25
秒。
esbuild 僅耗時 0.37
秒。
差別巨大。
還有更多對比:
webpack5 表示很受傷: 我還比不過 webpack 4 ?
...
有如下幾個緣由。
(爲了保證內容的準確性, 如下內容翻譯自 esbuild 官網。)
大多數打包器都是用 JavaScript 編寫的,可是對於 JIT 編譯
的語言來講,命令行應用程序擁有最差的性能表現。
每次運行打包器時,JavaScript VM 都會在沒有任何優化提示的狀況下看到打包程序的代碼。
在 esbuild 忙於解析 JavaScript 時,node 忙於解析打包程序的JavaScript。
到節點完成解析打包程序代碼的時間時,esbuild可能已經退出,您的打包程序甚至尚未開始打包。
另外,Go 是爲並行性
而設計的,而 JavaScript 不是。
Go在線程之間共享內存
,而JavaScript必須在線程之間序列化數據。
Go 和 JavaScript都有並行的垃圾收集器
,可是Go的堆在全部線程
之間共享,而對於JavaScript, 每一個JavaScript線程中都有一個單獨的堆
。
根據測試,這彷佛將 JavaScript worker 線程的並行能力減小了一半,大概是由於一半CPU核心正忙於爲另外一半收集垃圾。
esbuild 中的算法通過精心設計
,能夠充分利用CPU資源。
大體分爲三個階段:
解析
連接
代碼生成
解析
和代碼生成
是大部分工做,而且能夠徹底並行化
(連接在大多數狀況下是固有的串行任務)。
因爲全部線程共享內存
,所以當捆綁導入同一JavaScript庫的不一樣入口點時,能夠輕鬆地共享工做。
大多數現代計算機具備多內核
,所以並行性是一個巨大的勝利。
本身編寫全部內容, 而不是使用第三方庫,能夠帶來不少性能優點。
能夠從一開始就牢記性能,能夠確保全部內容都使用一致的數據結構來避免昂貴的轉換,而且能夠在必要時進行普遍的體系結構更改。缺點固然是多了不少工做。
例如,許多捆綁程序都使用官方的TypeScript編譯器做爲解析器。
可是,它是爲實現TypeScript編譯器團隊的目標而構建的,它們沒有將性能做爲頭等大事。
理想狀況下, 根據數據數據的長度, 編譯器的複雜度爲O(n).
若是要處理大量數據,內存訪問速度可能會嚴重影響性能。
對數據進行的遍歷次數越少(將數據轉換成數據所需的不一樣表示形式也就越少),編譯器就會越快。
例如,esbuild 僅觸及整個JavaScript AST 3次:
當 AST 數據在CPU緩存中仍然處於活躍狀態時,會最大化AST數據的重用。
其餘打包器在單獨的過程當中執行這些步驟,而不是將它們交織在一塊兒。
它們也能夠在數據表示之間進行轉換,將多個庫組織在一塊兒(例如:字符串→TS→JS→字符串,而後字符串→JS→舊的JS→字符串,而後字符串→JS→minified JS→字符串)。
這樣會佔用更多內存,而且會減慢速度。
Go的另外一個好處是它能夠將內容緊湊地存儲在內存中,從而使它可使用更少的內存並在CPU緩存中容納更多內容。
全部對象字段的類型和字段都緊密地包裝在一塊兒,例如幾個布爾標誌每一個僅佔用一個字節。
Go 還具備值語義,能夠將一個對象直接嵌入到另外一個對象中,所以它是'免費的',無需另外分配。
JavaScript不具備這些功能,還具備其餘缺點,例如 JIT 開銷(例如隱藏的類插槽)和低效的表示形式(例如,非整數與指針堆分配)。
以上的每一條因素, 都能在必定程度上提升編譯速度。
當它們共同工做時,效果比當今一般使用的其餘打包器快幾個數量級。
以上內容比較繁瑣,對此,也有一些網友作了簡要的總結:
Go
語言編寫的,該語言能夠編譯爲本地代碼。 並且 Go 的執行速度很快。 通常來講,JS 的操做是毫秒級
,而 Go 則是納秒級
。解析
,生成最終打包文件和生成 source maps 的操做所有徹底並行化無需昂貴的數據轉換
,只需不多的幾步便可完成全部操做以提升編譯速度爲編寫代碼時的第一原則
,並儘可能避免沒必要要的內存分配。僅做參考。
如下這幾個 feature 已經在進行中了, 並且是第一優先級:
Code splitting
(#16, docs)CSS content type
(#20, docs)Plugin API
(#111)下面這幾個 fearure 比較有潛力, 可是還不肯定:
HTML content type
(#31)Lowering to ES5
(#297)Bundling top-level await
(#253)感興趣的能夠保持關注。
vite
中大量使用了 esbuild
, 這裏簡單分享兩點。
optimizer
import { build, BuildOptions as EsbuildBuildOptions } from 'esbuild' // ... const result = await build({ entryPoints: Object.keys(flatIdDeps), bundle: true, format: 'esm', external: config.optimizeDeps?.exclude, logLevel: 'error', splitting: true, sourcemap: true, outdir: cacheDir, treeShaking: 'ignore-annotations', metafile: true, define, plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config) ], ...esbuildOptions }) const meta = result.metafile! // the paths in `meta.outputs` are relative to `process.cwd()` const cacheDirOutputPath = path.relative(process.cwd(), cacheDir) for (const id in deps) { const entry = deps[id] data.optimized[id] = { file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')), src: entry, needsInterop: needsInterop( id, idToExports[id], meta.outputs, cacheDirOutputPath ) } } writeFile(dataPath, JSON.stringify(data, null, 2))
.ts
文件
爲何生產環境仍需打包?
儘管原生 ESM
如今獲得了普遍支持
,但因爲嵌套導入會致使額外的網絡往返
,在生產環境中發佈未打包的 ESM 仍然效率低下(即便使用 HTTP/2
)。
爲了在生產環境中得到最佳的加載性能
,最好仍是將代碼進行 tree-shaking
、懶加載
和 chunk 分割
(以得到更好的緩存
)。
要確保開發
服務器和產品構建
之間的最佳輸出
和行爲
達到一致,並不容易。
爲解決這個問題,Vite 附帶了一套 構建優化
的 構建命令
,開箱即用。
爲什麼 vite 不用 esbuild 打包?
雖然 esbuild
快得驚人,而且已是一個在構建庫方面比較出色的工具,但一些針對構建應用的重要功能仍然還在持續開發中 —— 特別是代碼分割
和 CSS處理
方面。
就目前來講,Rollup
在應用打包方面, 更加成熟和靈活。
儘管如此,當將來這些功能穩定後,也不排除使用 esbuild 做爲生產構建器
的可能。
esbuild 爲構建提效帶來了曙光, 並且 esm 的數量也在快速增長:
但願 esm
生態儘快完善起來, 造福前端。
--
今天的內容就這麼多, 但願對你們有所啓發。
才疏學淺,文章如有錯誤, 歡迎指正, 謝謝。