FAQ
これはesbuildに関するよくある質問を集めたものです。 GitHubのissue trackerでも質問できます。
#なぜ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コアを完全に飽和させるように注意深く設計されています。解析、リンク、コード生成というおおよそ3つのフェーズがあります。解析とコード生成は作業の大部分を占め、完全に並列化可能です(リンクはほとんどの場合、本質的にシリアルなタスクです)。すべてのスレッドがメモリを共有するため、同じJavaScriptライブラリをインポートする異なるエントリポイントをバンドルする場合、作業を簡単に共有できます。最新のコンピューターは多くのコアを持っているため、並列処理は大きなメリットとなります。
esbuildのすべてはゼロから書かれています。
サードパーティのライブラリを使用する代わりに、すべてを自分で記述することには多くのパフォーマンス上の利点があります。最初からパフォーマンスを考慮でき、高価な変換を避けるためにすべてが一貫したデータ構造を使用していることを確認でき、必要に応じて広範囲なアーキテクチャ変更を行うことができます。欠点は、もちろん、それが多くの作業であるということです。
たとえば、多くのバンドラは公式のTypeScriptコンパイラをパーサーとして使用しています。しかし、それはTypeScriptコンパイラチームの目標に役立つように構築されており、パフォーマンスを最優先事項としていません。彼らのコードは、メガモルフィックなオブジェクト形状と不要な動的なプロパティアクセス(どちらもよく知られたJavaScriptの速度低下要因)をかなり多用しています。そして、TypeScriptパーサーは、型チェックが無効になっている場合でも型チェッカーを実行しているようです。esbuildのカスタムTypeScriptパーサーでは、これらはすべて問題になりません。
メモリが効率的に使用されています。
コンパイラは理想的には、入力の長さでO(n)の複雑さになります。そのため、大量のデータ処理を行う場合、メモリアクセスの速度がパフォーマンスに大きく影響する可能性があります。データに対して行う必要があるパスが少なく(また、データを変換する必要がある表現も少ない)、コンパイラの速度が向上します。
たとえば、esbuildはJavaScript AST全体を3回しか触りません
- 字句解析、構文解析、スコープ設定、およびシンボルの宣言を行うパス
- シンボルのバインディング、構文の縮小、JSX/TSからJSへの変換、およびESNextからES2015への変換を行うパス
- 識別子の縮小、空白の縮小、コードの生成、およびソースマップの生成を行うパス
これにより、CPUキャッシュにまだホットな状態にあるASTデータの再利用が最大化されます。他のバンドラは、これらのステップをインターリーブするのではなく、個別のパスで行います。また、複数のライブラリを組み合わせるためにデータ表現間を変換することもあります(例:string→TS→JS→string、次にstring→JS→古いJS→string、次にstring→JS→縮小されたJS→string)。これにより、より多くのメモリが使用され、速度が低下します。
Goのもう1つの利点は、メモリにコンパクトに格納できることです。これにより、メモリ使用量が少なくなり、CPUキャッシュにより多くのデータを格納できます。すべてのオブジェクトフィールドには型があり、フィールドは密に詰め込まれているため、たとえば、いくつかのブール型のフラグはそれぞれ1バイトしかかかりません。Goには値セマンティクスがあり、1つのオブジェクトを別のオブジェクトに直接埋め込むことができるため、「無料」で別の割り当てを行う必要がありません。JavaScriptにはこれらの機能がなく、JITオーバーヘッド(例:隠しクラススロット)や非効率的な表現(例:整数以外の数値はポインタを使用してヒープに割り当てられる)などの欠点もあります。
これらの要因のそれぞれは、ある程度の速度向上にすぎませんが、これらを組み合わせることで、現在一般的に使用されている他のバンドラよりも桁違いに高速なバンドラを実現できます。
#ベンチマークの詳細
各ベンチマークの詳細を以下に示します。
このベンチマークは、three.jsライブラリを10回複製し、キャッシュを使用せずにゼロから単一のバンドルをビルドすることにより、大規模なJavaScriptコードベースを近似しています。このベンチマークは、esbuildリポジトリで`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 |
報告された時間はすべて3回の実行のうち最良の結果です。esbuildは`--bundle --minify --sourcemap`で実行しています。`@rollup/plugin-terser`プラグインを使用したのは、Rollup自体がミニファイをサポートしていないためです。Webpack 5は`--mode=production --devtool=sourcemap`を使用します。Parcel 2はデフォルトのオプションを使用します。絶対速度は、コメントと空白行を含む合計行数に基づいており、現在547,441行です。テストは、16GBのRAMを搭載した6コアの2019 MacBook Proで、macOS Spotlightを無効にして行われました。
このベンチマークは、古いRomeコードベース(Rustによる書き換え前)を使用して、大規模なTypeScriptコードベースを近似しています。すべてのコードを単一のミニファイされたバンドルにソースマップ付きで結合する必要があり、結果のバンドルは正しく機能する必要があります。このベンチマークは、esbuildリポジトリで`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 |
報告された時間はすべて3回の実行のうち最良の結果です。esbuildは`--bundle --minify --sourcemap --platform=node`で実行しています。Webpack 5は`ts-loader`を`transpileOnly: true`および`--mode=production --devtool=sourcemap`で使用しています。Parcel 2は`package.json`で`"engines": "node"`を使用します。絶対速度は、コメントと空白行を含む合計行数に基づいており、現在131,836行です。テストは、16GBのRAMを搭載した6コアの2019 MacBook Proで、macOS Spotlightを無効にして行われました。
TypeScriptコンパイルに関連する理由で動作させることができなかったため、結果はRollupを含んでいません。`@rollup/plugin-typescript`を試しましたが、型チェックを無効にすることができず、`@rollup/plugin-sucrase`を試しましたが、`tsconfig.json`ファイル(正しいパス解決に必要)を提供する方法がありません。
#今後のロードマップ
これらの機能はすでに進行中で、最優先事項です。
これらは将来の潜在的な機能ですが、実現しない場合や、より限定的な範囲で実現する場合があります。
- HTMLコンテンツタイプ(#31)
その時点以降、esbuildは比較的完成していると見なされます。esbuildがほぼ安定した状態に達し、それ以上の機能の追加を停止することを計画しています。これには、esbuild自体に主要な機能を追加する要求を「拒否する」ことが含まれます。esbuildはすべてのフロントエンドニーズに対するオールインワンのソリューションになるべきではないと考えています。特に、「webpack config」モデルの問題点と苦痛を避けたいと考えています。このモデルでは、基盤となるツールが柔軟すぎるため、使いやすさが損なわれます。
たとえば、esbuildのコア自体には次の機能を含める予定はありません。
- その他のフロントエンド言語のサポート(例:Elm、Svelte、Vue、Angular)
- TypeScript型チェック(別途`tsc`を実行してください)
- カスタムAST操作のためのAPI
- ホットモジュールリロード
- モジュールフェデレーション
esbuild に追加している拡張ポイント(プラグインとAPI)により、よりカスタマイズされたビルドワークフローの一部として esbuild を使用できるようになることを願っていますが、これらの拡張ポイントですべてのユースケースを網羅することを意図したり、期待したりしていません。非常に特殊な要件がある場合は、他のツールを使用する必要があります。また、esbuild が他のビルドツールの劇的なパフォーマンス向上を促し、実装を見直すことで、esbuild を使用する人だけでなく、すべての人が恩恵を受けられるようになると期待しています。
esbuild が安定版に到達した後も、esbuild の既存の範囲内のすべてのものを維持し続ける予定です。たとえば、新しくリリースされた JavaScript と TypeScript の構文機能のサポートを実装することを意味します。
#本番環境への対応
このプロジェクトはまだバージョン 1.0.0 に到達しておらず、開発中です。とはいえ、アルファ段階をはるかに超えており、かなり安定しています。後期ベータ版と考えています。一部の先進的な利用者にとっては、実運用に十分なレベルです。一方、まだ準備が整っていないと考える人もいます。このセクションでは、どちらの意見にも説得しようとはしません。esbuild をバンドラとして使用するかどうかの判断に必要な情報を提供することを目的としています。
いくつかのデータポイント
- 他のプロジェクトで使用されている状況
API は既に多くの他の開発者ツール内でライブラリとして使用されています。たとえば、Vite と Snowpack は TypeScript を JavaScript に変換するために esbuild を使用しており、Amazon CDK(クラウド開発キット)と Phoenix はコードをバンドルするために esbuild を使用しています。
- API の安定性
esbuild のバージョンはまだ 1.0.0 ではありませんが、API の安定性を維持するための努力が続けられています。パッチバージョンは下位互換性のある変更を目的とし、マイナーバージョンは下位互換性のない変更を目的としています。esbuild を本番環境で使用する予定がある場合は、正確なバージョンを固定する(最大の安全性)か、メジャーバージョンとマイナーバージョンを固定する(下位互換性のあるアップグレードのみを受け入れる)必要があります。
- 主要開発者は一人だけ
このツールは主に私によって構築されています。一部の人にとってはこれで問題ありませんが、他の人にとっては、esbuild が組織に適したツールではないことを意味します。それは構いません。esbuild を構築するのは楽しいですし、私自身が使用したいツールだからです。それを世界と共有するのは、他のユーザーもいるから、フィードバックによってツール自体が向上するから、そしてエコシステムがより良いツールを作るインスピレーションになると思うからです。
- スコープの拡大には常にオープンではない
構築および/または維持することに興味のない主要な機能を含める予定はありません。また、アーキテクチャの観点から、テストと正確性の観点から、そして使いやすさの観点から、プロジェクトの範囲が複雑になりすぎないように制限したいと考えています。esbuild をWebのための「リンカ」と考えてください。JavaScriptとCSSを変換およびバンドルする方法を理解しています。しかし、ソースコードがプレーンなJavaScriptまたはCSSになる方法の詳細は、サードパーティコードである必要があるかもしれません。
プラグインにより、コミュニティがesbuild自体に貢献する必要なしに主要な機能(例:WebAssemblyインポート)を追加できることを期待しています。ただし、プラグインAPIですべてが公開されているわけではなく、追加したい特定の機能をesbuildに追加できない場合があります。これは意図的なものです。esbuildはすべてのフロントエンドニーズに対するオールインワンのソリューションとなることを意図していません。
#ウイルス対策ソフトウェア
esbuild はネイティブコードで記述されているため、ウイルス対策ソフトウェアが誤ってウイルスとしてフラグを立てることがあります。これは esbuild がウイルスであることを意味するものではありません。私は悪意のあるコードを公開しておらず、サプライチェーンのセキュリティを非常に真剣に受け止めています。
esbuild のコードの大部分は、Google の追加 Go パッケージへの1 つの依存関係を除いて、ファーストパーティコードです。私の開発作業は、ビルドを公開するために使用するマシンとは別の、隔離されたマシンで行われています。esbuild の公開ビルドが完全に再現可能であることを確認するための追加作業を行い、リリース後には、公開されたビルドが自動的に比較され、関連のない環境でローカルに構築されたビルドとビット単位で同一であることが確認されます(つまり、Go コンパイラ自体が侵害されていないことを意味します)。ソースから自分で esbuild をビルドし、ビルド成果物を公開されたものと比較して、これを独立して検証することもできます。
誤検知に対処しなければならないのは、ウイルス対策ソフトウェアを使用する際の不幸な現実です。ウイルス対策ソフトが esbuild を使用できない場合の回避策をいくつか示します。
- ウイルス対策ソフトウェアを無視し、esbuild を検疫から削除する
- 特定の esbuild ネイティブ実行ファイルを誤検知としてウイルス対策ソフトウェアベンダーに報告する
- ウイルス対策ソフトウェアをバイパスするために、
esbuild
の代わりにesbuild-wasm
を使用します(ネイティブ実行ファイルと同じように 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 である Go 1.13 を使用してビルドされていると不満を言うことがあります。しかし、それは正しくありません。go.mod
のその行は、最小のコンパイラバージョンのみを指定します。esbuild の公開バイナリがビルドされている Go のバージョンとは関係ありません。これは、はるかに新しいバージョンの Go です。ドキュメントをお読みください。
また、esbuild が使用するバージョン(特に GO-2022-0493、Faccessat
関数に関するもの)に既知の脆弱性があるため、golang.org/x/sys
の依存関係を更新することを求める人もいます。esbuild が新しいバージョンの golang.org/x/sys
依存関係に更新できない理由は、新しいバージョンが unsafe.Slice
関数を使用し始めたことであり、これは Go 1.17 で初めて導入されたため(そのため、古いバージョンの Go ではコンパイルされません)。ただし、この脆弱性レポートは、a) esbuild はそもそもその関数を呼び出さないこと、b) esbuild はビルドツールでありサンドボックスではなく、esbuild のファイルシステムアクセスはセキュリティに敏感ではないため、無関係です。
古いプラットフォームとの互換性を維持せず、無関係な脆弱性レポートを回避するために一部の人が esbuild を使用できなくすることはありません。上記の記述されている問題に関するレポートは無視してください。
#圧縮された改行
esbuild のミニファイアが通常、JavaScript 文字列内の文字エスケープシーケンス \n
をテンプレートリテラルの改行文字に変更することに驚く人がいます。しかし、これは意図的なものです。**これは esbuild のバグではありません。**ミニファイアの役割は、入力と同等の可能な限りコンパクトな出力を生成することです。文字エスケープシーケンス \n
は 2 バイト長ですが、改行文字は 1 バイト長です。
たとえば、このコードは 21 バイト長です。
var text="a\nb\nc\n";
一方、このコードは 18 バイト長です。
var text=`a
b
c
`;
したがって、2 番目のコードは完全にミニファイされているのに対し、最初のコードはそうではありません。コードのミニファイは、すべてを 1 行にすることを意味するものではありません。代わりに、可能な限り少ないバイト数を使用する同等のコードを生成することを意味します。JavaScript では、タグなしテンプレートリテラルは文字列リテラルと同等であるため、esbuild はここで正しいことを行っています。
#名前の衝突の回避
エントリポイントモジュールの最上位レベルの変数は、esbuild の出力をブラウザで実行する場合、グローバルスコープに決して存在してはなりません。それが発生する場合は、esbuild の出力形式に関するドキュメントに従っておらず、esbuild を正しく使用していないことを意味します。**これは esbuild のバグではありません。**
具体的には、esbuild の出力をブラウザで実行する場合は、次のいずれかを行う必要があります。
--format=
とiife <script
src="..."> コードをグローバルスコープで実行する場合は、
--format=
を使用する必要があります。これにより、esbuild の出力によってコードがラップされ、最上位レベルの変数がネストされたスコープで宣言されます。iife --format=
とesm <script
src="..." type="module"> --format=
を使用する場合は、コードをモジュールとして実行する必要があります。これにより、ブラウザによってコードがラップされ、最上位レベルの変数がネストされたスコープで宣言されます。esm
<script
で --format=
を使用すると、コードが微妙で分かりにくい方法で壊れます(type="
を省略すると、最上位レベルの変数はすべてグローバルスコープに存在することになり、他の JavaScript ファイルの同じ名前の最上位レベルの変数と衝突します)。
#最上位レベルの var
esbuild が最上位レベルの let
、const
、class
宣言を var
宣言として書き換えることがあることに驚く人がいます。これはいくつかの理由で行われます。
- 正確性のため
バンドリングでは、モジュールを遅延初期化することが必要になる場合があります。たとえば、バンドル内のモジュールのパスを使用して
require()
またはimport()
を呼び出す場合です。これには、最上位レベルのシンボルの宣言と初期化を分離し、初期化をクロージャに移動することが含まれます。そのため、たとえばclass
ステートメントは、クラス式を変数への代入として書き換えられます。遅延初期化クロージャから宣言を維持することは、パフォーマンスにとって重要です。他のモジュールが名前で直接参照できるようになるため、より遅いプロパティアクセスを介して間接的に参照する必要がなくなるためです。これが必要なもう 1 つのケースは、最上位レベルの
using
宣言を変換する場合です。これには、モジュール本体全体をtry
ブロックでラップすることが含まれ、最上位レベルのシンボルの宣言と初期化も分離されます。最上位レベルのシンボルをエクスポートする必要があるため、try
ブロック内で宣言することはできません。これらの両方の場合、esbuild はソースコードに
const
シンボルの変更が含まれている場合にビルドエラーで失敗するため、esbuild による最上位レベルのconst
のvar
への書き換えによって定数の変更が発生することはありません。esbuild の現在のアーキテクチャでは、この変換を行う esbuild の部分(パーサー)は、現在のモジュールが遅延初期化されるかどうかを知ることができません。この決定の情報は、ビルドの後期にしか発見されない場合があり、同じ AST を再利用する将来の増分ビルドで変更される場合もあります(ファイルごとの AST は解析時に 1 回変換され、増分ビルド全体でキャッシュされ、再利用されます)。そのため、バンドリングがアクティブな場合は常にこの変換が行われます。
- パフォーマンスのため
複数の JavaScript VM は、TDZ(つまり「一時的デッドゾーン」)チェックでパフォーマンスの問題を抱えてきました。これらのチェックは、let、const、またはクラスのシンボルが初期化される前に使用されていないことを検証します。有名な VM に関する 2 つの問題を次に示します。
- 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チェックが必要な変数の数に対して2乗オーダーになっていたため、深刻なパフォーマンス問題を抱えていました(最上位スコープが典型的に最も深刻な問題を引き起こしていました)。V8は、既に同じ関数内でより前にチェック済みであった場合や、問題の関数が既に実行済みであった場合(つまり、チェックが既に済んでいる場合)でも、生成されるJITコード全体にTDZチェックが存在するという、継続的な問題を抱えています。
JavaScriptでは、`let`、`const`、`class`宣言はすべてTDZチェックを導入しますが、`var`宣言は導入しません。バンドリングは通常、多くのモジュールを単一の非常に大きな最上位スコープにマージするため、これらのTDZチェックのパフォーマンスへの影響は非常に深刻なものとなり得ます。最上位の`let`、`const`、`class`宣言を`var`に変換することで、コードを自動的に高速化できます。
esbuildは、モジュールが遅延初期化される必要がある可能性があるため(上記の説明を参照)、宣言と初期化を分離する必要があるため、最上位のTDZ副作用を保持しません。最上位のシンボルに対するTDZチェックは、仮に、最上位のシンボルを使用する前にチェックを行い、初期化されていない場合は例外をスローする追加コードを生成することで(事実上、実際のJavaScript VMが行うことを手動で実装することで)サポートできる可能性があります。しかし、これはコードサイズと実行時間の両方のオーバーヘッドが過大であり、本番環境向けのバンドラが実行すべきことではないと思われます。