esbuild 爲何這麼快

前言

esbuild 是新一代的 JavaScript 打包工具。前端

他的做者是 Figma 的 CTO - Evan Wallacenode

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 秒。

差別巨大。

還有更多對比:

https://twitter.com/evanwallace/status/1314121407903617025

webpack5 表示很受傷: 我還比不過 webpack 4 ?

...

爲何 esbuild 這麼快 ?

有如下幾個緣由。

(爲了保證內容的準確性, 如下內容翻譯自 esbuild 官網。)

1. 它是用 Go 語言編寫的,並能夠編譯爲本地代碼。

大多數打包器都是用 JavaScript 編寫的,可是對於 JIT 編譯的語言來講,命令行應用程序擁有最差的性能表現。

每次運行打包器時,JavaScript VM 都會在沒有任何優化提示的狀況下看到打包程序的代碼。

在 esbuild 忙於解析 JavaScript 時,node 忙於解析打包程序的JavaScript。

到節點完成解析打包程序代碼的時間時,esbuild可能已經退出,您的打包程序甚至尚未開始打包。

另外,Go 是爲並行性而設計的,而 JavaScript 不是。

Go在線程之間共享內存,而JavaScript必須在線程之間序列化數據。

Go 和 JavaScript都有並行的垃圾收集器,可是Go的堆在全部線程之間共享,而對於JavaScript, 每一個JavaScript線程中都有一個單獨的堆

根據測試,這彷佛將 JavaScript worker 線程的並行能力減小了一半,大概是由於一半CPU核心正忙於爲另外一半收集垃圾。

2. 大量使用了並行操做。

esbuild 中的算法通過精心設計,能夠充分利用CPU資源。

大體分爲三個階段:

  1. 解析
  2. 連接
  3. 代碼生成

解析代碼生成是大部分工做,而且能夠徹底並行化(連接在大多數狀況下是固有的串行任務)。

因爲全部線程共享內存,所以當捆綁導入同一JavaScript庫的不一樣入口點時,能夠輕鬆地共享工做。

大多數現代計算機具備多內核,所以並行性是一個巨大的勝利。

3. 代碼都是本身寫的, 沒有使用第三方依賴。

本身編寫全部內容, 而不是使用第三方庫,能夠帶來不少性能優點。

能夠從一開始就牢記性能,能夠確保全部內容都使用一致的數據結構來避免昂貴的轉換,而且能夠在必要時進行普遍的體系結構更改。缺點固然是多了不少工做。

例如,許多捆綁程序都使用官方的TypeScript編譯器做爲解析器。

可是,它是爲實現TypeScript編譯器團隊的目標而構建的,它們沒有將性能做爲頭等大事。

4. 內存的高效利用。

理想狀況下, 根據數據數據的長度, 編譯器的複雜度爲O(n).

若是要處理大量數據,內存訪問速度可能會嚴重影響性能。

對數據進行的遍歷次數越少(將數據轉換成數據所需的不一樣表示形式也就越少),編譯器就會越快。

例如,esbuild 僅觸及整個JavaScript AST 3次:

  1. 進行詞法分析,解析,做用域設置和聲明符號的過程
  2. 綁定符號,最小化語法。 好比:將 JSX / TS轉換爲 JS, ES Next 轉換爲 es5。
  3. 最小標識符,最小空格,生成代碼。

當 AST 數據在CPU緩存中仍然處於活躍狀態時,會最大化AST數據的重用。

其餘打包器在單獨的過程當中執行這些步驟,而不是將它們交織在一塊兒。

它們也能夠在數據表示之間進行轉換,將多個庫組織在一塊兒(例如:字符串→TS→JS→字符串,而後字符串→JS→舊的JS→字符串,而後字符串→JS→minified JS→字符串)。

這樣會佔用更多內存,而且會減慢速度。

Go的另外一個好處是它能夠將內容緊湊地存儲在內存中,從而使它可使用更少的內存並在CPU緩存中容納更多內容。

全部對象字段的類型和字段都緊密地包裝在一塊兒,例如幾個布爾標誌每一個僅佔用一個字節。

Go 還具備值語義,能夠將一個對象直接嵌入到另外一個對象中,所以它是'免費的',無需另外分配。

JavaScript不具備這些功能,還具備其餘缺點,例如 JIT 開銷(例如隱藏的類插槽)和低效的表示形式(例如,非整數與指針堆分配)。

以上的每一條因素, 都能在必定程度上提升編譯速度。

當它們共同工做時,效果比當今一般使用的其餘打包器快幾個數量級。

以上內容比較繁瑣,對此,也有一些網友作了簡要的總結:

  • 它是用 Go 語言編寫的,該語言能夠編譯爲本地代碼。 並且 Go 的執行速度很快。 通常來講,JS 的操做是毫秒級,而 Go 則是納秒級
  • 解析,生成最終打包文件和生成 source maps 的操做所有徹底並行化
  • 無需昂貴的數據轉換,只需不多的幾步便可完成全部操做
  • 該庫以提升編譯速度爲編寫代碼時的第一原則,並儘可能避免沒必要要的內存分配。

僅做參考。

Upcoming roadmap

如下這幾個 feature 已經在進行中了, 並且是第一優先級:

  1. Code splitting (#16, docs)
  2. CSS content type (#20, docs)
  3. Plugin API (#111)

下面這幾個 fearure 比較有潛力, 可是還不肯定:

  1. HTML content type (#31)
  2. Lowering to ES5 (#297)
  3. Bundling top-level await (#253)

感興趣的能夠保持關注。

esbuild 在 vite 中的運用

vite 中大量使用了 esbuild, 這裏簡單分享兩點。

  1. optimizer

https://github.com/vitejs/vite/blob/main/packages/vite/src/node/optimizer/index.ts#L262

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))
  1. 處理 .ts 文件

https://github.com/vitejs/vite/commit/59035546db7ff4b7020242ba994a5395aac92802

爲何生產環境仍需打包?

儘管原生 ESM 如今獲得了普遍支持,但因爲嵌套導入會致使額外的網絡往返,在生產環境中發佈未打包的 ESM 仍然效率低下(即便使用 HTTP/2)。

爲了在生產環境中得到最佳的加載性能,最好仍是將代碼進行 tree-shaking懶加載chunk 分割(以得到更好的緩存)。

要確保開發服務器和產品構建之間的最佳輸出行爲達到一致,並不容易。

爲解決這個問題,Vite 附帶了一套 構建優化構建命令,開箱即用。

爲什麼 vite 不用 esbuild 打包?

雖然 esbuild 快得驚人,而且已是一個在構建庫方面比較出色的工具,但一些針對構建應用的重要功能仍然還在持續開發中 —— 特別是代碼分割CSS處理方面。

就目前來講,Rollup 在應用打包方面, 更加成熟和靈活。

儘管如此,當將來這些功能穩定後,也不排除使用 esbuild 做爲生產構建器的可能。

總結

esbuild 爲構建提效帶來了曙光, 並且 esm 的數量也在快速增長:

https://twitter.com/skypackjs/status/1113838647487287296

但願 esm 生態儘快完善起來, 造福前端。

--

今天的內容就這麼多, 但願對你們有所啓發。

才疏學淺,文章如有錯誤, 歡迎指正, 謝謝。

參考連接

  1. https://esbuild.github.io/get...
  2. https://esbuild.github.io/faq/
  3. https://twitter.com/skypackjs...
相關文章
相關標籤/搜索