バイトコードキャッシングは、JavaScript をバイトコードに事前コンパイルすることでアプリケーションの起動時間を大幅に改善するビルド時最適化です。例えば、バイトコードを有効にして TypeScript の tsc をコンパイルすると、起動時間が2 倍改善されます。
使用方法
基本的な使用方法
--bytecode フラグでバイトコードキャッシングを有効にします:
bun build ./index.ts --target=bun --bytecode --outdir=./distこれにより、2 つのファイルが生成されます:
dist/index.js- バンドルされた JavaScriptdist/index.jsc- バイトコードキャッシュファイル
ランタイムでは、Bun は自動的に .jsc ファイルを検出して使用します:
bun ./dist/index.js # 自動的に index.jsc を使用スタンドアロン実行可能ファイルの場合
--compile で実行可能ファイルを作成する際、バイトコードはバイナリに埋め込まれます:
bun build ./cli.ts --compile --bytecode --outfile=mycli結果の実行可能ファイルにはコードとバイトコードの両方が含まれており、単一ファイルで最大のパフォーマンスを実現します。
他の最適化との組み合わせ
バイトコードはミニファイやソースマップと併用できます:
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli--minifyはバイトコード生成前にコードサイズを削減します(コードが少ないほどバイトコードも少なくなります)--sourcemapはエラー報告を維持します(エラーは元のソースを指します)--bytecodeは解析オーバーヘッドを排除します
パフォーマンスへの影響
パフォーマンスの改善はコードベースのサイズに比例します:
| アプリケーションサイズ | 典型的な起動時の改善 |
|---|---|
| 小規模 CLI(< 100 KB) | 1.5-2 倍高速 |
| 中〜大規模アプリ(> 5 MB) | 2.5-4 倍高速 |
アプリケーションが大きいほど、解析するコードが多いため、より大きな恩恵を受けます。
バイトコードを使用するタイミング
適している用途:
CLI ツール
- 頻繁に呼び出される(リンター、フォーマッター、git フック)
- 起動時間がユーザーエクスペリエンス全体
- ユーザーは 90ms と 45ms の起動時間の違いに気づく
- 例:TypeScript コンパイラー、Prettier、ESLint
ビルドツールとタスクランナー
- 開発中に何百〜何千回も実行される
- 1 回あたりのミリ秒の節約がすぐに蓄積する
- 開発者エクスペリエンスの改善
- 例:ビルドスクリプト、テストランナー、コードジェネレーター
スタンドアロン実行可能ファイル
- 迅速なパフォーマンスを重視するユーザーに配布される
- 単一ファイルの配布は便利
- ファイルサイズよりも起動時間が重要
- 例:npm またはバイナリとして配布される CLI
不要な用途:
- ❌ 小規模スクリプト
- ❌ 1 回だけ実行されるコード
- ❌ 開発ビルド
- ❌ サイズ制約のある環境
- ❌ トップレベル await を使用するコード(サポートされていません)
制限事項
CommonJS のみ
バイトコードキャッシングは現在、CommonJS 出力形式でのみ機能します。Bun のバンドラーはほとんどの ESM コードを自動的に CommonJS に変換しますが、トップレベル await は例外です:
// これによりバイトコードキャッシングが防止されます
const data = await fetch("https://api.example.com");
export default data;理由:トップレベル await は非同期モジュール評価を必要とし、これは CommonJS で表現できません。モジュールグラフは非同期になり、CommonJS ラッパー関数モデルは機能しなくなります。
回避策:非同期初期化を関数内に移動します:
async function init() {
const data = await fetch("https://api.example.com");
return data;
}
export default init;これで、モジュールはコンシューマーが必要に応じて await できる関数をエクスポートします。
バージョン互換性
バイトコードは Bun バージョン間で移植できません。バイトコード形式は JavaScriptCore の内部表現に紐付いており、バージョン間で変更されます。
Bun を更新したら、バイトコードを再生成する必要があります:
# Bun 更新後
bun build --bytecode ./index.ts --outdir=./distバイトコードが現在の Bun バージョンと一致しない場合、自動的に無視され、コードは JavaScript ソースの解析にフォールバックします。アプリは実行されますが、パフォーマンス最適化は失われます。
ベストプラクティス:CI/CD ビルドプロセスの一部としてバイトコードを生成します。.jsc ファイルを git にコミットしないでください。Bun を更新するたびに再生成します。
ソースコードは依然として必要
.jsファイル(バンドルされたソースコード).jscファイル(バイトコードキャッシュ)
ランタイムでは:
- Bun は
.jsファイルを読み込み、@bytecodeプラグマを確認し、.jscファイルをチェックします - Bun は
.jscファイルを読み込みます - Bun はバイトコードハッシュがソースと一致することを検証します
- 有効な場合、Bun はバイトコードを使用します
- 無効な場合、Bun はソースの解析にフォールバックします
バイトコードは難読化ではありません
バイトコードはソースコードを難読化しません。これは最適化であり、セキュリティ対策ではありません。
本番環境デプロイ
Docker
Dockerfile にバイトコード生成を含めます:
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --bytecode --minify --sourcemap \
--target=bun \
--outdir=./dist \
--compile \
./src/server.ts --outfile=./dist/server
FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /dist/server /app/server
CMD ["./server"]バイトコードはアーキテクチャに依存しません。
CI/CD
ビルドパイプライン中にバイトコードを生成します:
# GitHub Actions
- name: Build with bytecode
run: |
bun install
bun build --bytecode --minify \
--outdir=./dist \
--target=bun \
./src/index.tsデバッグ
バイトコードが使用されていることを確認
.jsc ファイルが存在することを確認します:
ls -lh dist/-rw-r--r-- 1 user staff 245K index.js
-rw-r--r-- 1 user staff 1.1M index.jsc.jsc ファイルは .js ファイルの 2-8 倍のサイズになります。
バイトコードが使用されているかどうかをログ出力するには、環境で BUN_JSC_verboseDiskCache=1 を設定します。
成功すると、次のようなログが出力されます:
[Disk cache] cache hit for sourceCodeキャッシュミスがある場合は、次のようなログが出力されます:
[Disk cache] cache miss for sourceCodeBun は現在組み込みモジュールで使用される JavaScript コードをバイトコードキャッシュしないため、キャッシュミスが複数回ログ出力されるのは正常です。
一般的な問題
バイトコードがサイレントに無視される:通常は Bun バージョンの更新が原因です。キャッシュバージョンが一致しないため、バイトコードが拒否されます。再生成して修正します。
ファイルサイズが大きすぎる:これは予想通りです。次のことを検討してください:
- バイトコード生成前に
--minifyを使用してコードサイズを削減する - ネットワーク転送用に
.jscファイルを圧縮する(gzip/brotli) - 起動パフォーマンスの改善がサイズ増加に見合う価値があるかを評価する
トップレベル await:サポートされていません。非同期初期化関数を使用するようにリファクタリングします。
バイトコードとは
JavaScript を実行するとき、JavaScript エンジンはソースコードを直接実行するわけではありません。代わりに、いくつかのステップを踏みます:
- 解析:エンジンは JavaScript ソースコードを読み取り、抽象構文木(AST)に変換します
- バイトコードコンパイル:AST はバイトコードにコンパイルされます - これは実行がより高速な低レベル表現です
- 実行:バイトコードはエンジンのインタープリターまたは JIT コンパイラーによって実行されます
バイトコードは中間表現です - JavaScript ソースコードよりも低レベルですが、マシンコードよりも高レベルです。仮想マシンのアセンブリ言語のようなものだと考えてください。各バイトコード命令は、「この変数を読み込む」「2 つの数を加算する」「この関数を呼び出す」などの単一操作を表します。
これはコードを実行するたびに発生します。1 日に 100 回実行される CLI ツールがある場合、コードは 100 回解析されます。頻繁なコールドスタートを伴うサーバーレス関数がある場合、コールドスタートごとに解析が発生します。
バイトコードキャッシングを使用すると、Bun はステップ 1 と 2 をビルドステップに移動します。ランタイムでは、エンジンは事前コンパイルされたバイトコードを読み込み、実行に直接移行します。
レイジー解析がこれをさらに良くする理由
モダンな JavaScript エンジンはレイジー解析と呼ばれる巧妙な最適化を使用します。すべてのコードを事前に解析するのではなく、関数は初めて呼び出されたときのみ解析されます:
// バイトコードキャッシングなし:
function rarely_used() {
// この 500 行の関数は実際に
// 呼び出されたときのみ解析されます
}
function main() {
console.log("Starting app");
// rarely_used() は決して呼び出されないため、解析されません
}これは、解析オーバーヘッドは起動コストだけでなく、異なるコードパスが実行されるにつれてアプリケーションのライフサイクル全体で発生することを意味します。バイトコードキャッシングを使用すると、すべての関数が事前コンパイルされます。これはレイジー解析される関数も含みます。解析作業はアプリケーションの実行中に分散されるのではなく、ビルド時に 1 回行われます。
バイトコード形式
.jsc ファイルの内部
.jsc ファイルにはシリアライズされたバイトコード構造が含まれています。内部を理解することで、パフォーマンスの利点とファイルサイズのトレードオフの両方が説明できます。
ヘッダーセクション(読み込みごとに検証):
- キャッシュバージョン:JavaScriptCore フレームワークバージョンに紐付いたハッシュ。これにより、Bun のあるバージョンで生成されたバイトコードはその正確なバージョンでのみ実行できます。
- コードブロックタイプタグ:これがプログラム、モジュール、eval、または関数コードブロックであるかを識別します。
SourceCodeKey(バイトコードがソースと一致することを検証):
- ソースコードハッシュ:元の JavaScript ソースコードのハッシュ。Bun はバイトコードを使用する前にこれが一致することを確認します。
- ソースコード長:追加の検証のためのソースの正確な長さ。
- コンパイルフラグ:厳格モード、スクリプト対モジュール、eval コンテキストタイプなどの重要なコンパイルコンテキスト。異なるフラグでコンパイルされた同じソースコードは、異なるバイトコードを生成します。
バイトコード命令:
- 命令ストリーム:実際のバイトコードオペコード - JavaScript のコンパイルされた表現。これは可変長のバイトコード命令シーケンスです。
- メタデータテーブル:各オペコードには関連するメタデータがあります - プロファイリングカウンター、型ヒント、実行カウントなど(まだ入力されていなくても)。
- ジャンプターゲット:制御フロー(if/else、ループ、switch 文)の事前計算されたアドレス。
- スイッチテーブル:switch 文の最適化されたルックアップテーブル。
定数と識別子:
- 定数プール:コード内のすべてのリテラル値 - 数値、文字列、ブール値、null、undefined。これらは実際の JavaScript 値(JSValues)として保存されるため、ランタイムでソースから解析する必要がありません。
- 識別子テーブル:コードで使用されるすべての変数と関数名。重複除外された文字列として保存されます。
- ソースコード表現マーカー:定数をどのように表現するか(整数、ダブル、ビッグイントなど)を示すフラグ。
関数メタデータ(コード内の各関数):
- レジスタ割り当て:関数が必要とするレジスタ(ローカル変数)の数 -
thisRegister、scopeRegister、numVars、numCalleeLocals、numParameters。 - コード機能:コンストラクターか?アロー関数か?
superを使用するか?テールコールがあるか?などの関数の特性のビットマスク。これらは関数の実行方法に影響します。 - 語彙スコープ機能:厳格モードなどの語彙コンテキスト。
- 解析モード:関数が解析されたモード(通常、非同期、ジェネレーター、非同期ジェネレーター)。
ネストされた構造:
- 関数宣言と式:各ネストされた関数は再帰的に独自のバイトコードブロックを取得します。100 の関数があるファイルには、構造内にネストされた 100 の個別のバイトコードブロックがあります。
- 例外ハンドラー:境界とハンドラーアドレスが事前計算された try/catch/finally ブロック。
- 式情報:エラー報告とデバッグのためにバイトコード位置をソースコード位置にマッピングします。
バイトコードに含まれていないもの
重要なこととして、バイトコードにはソースコードは埋め込まれていません。代わりに:
- JavaScript ソースは別々に(
.jsファイルに)保存されます - バイトコードはソースのハッシュと長さのみを保存します
- 読み込み時に、Bun はバイトコードが現在のソースコードと一致することを検証します
これが、.js ファイルと .jsc ファイルの両方をデプロイする必要がある理由です。.jsc ファイルは対応する .js ファイルなしでは役に立ちません。
トレードオフ:ファイルサイズ
バイトコードファイルはソースコードよりも大幅に大きくなります - 通常 2-8 倍大きくなります。
バイトコードがはるかに大きい理由
バイトコード命令は冗長です: ミニファイされた JavaScript の 1 行は、数十のバイトコード命令にコンパイルされる可能性があります。例えば:
const sum = arr.reduce((a, b) => a + b, 0);これは次のバイトコードにコンパイルされます:
arr変数を読み込むreduceプロパティを取得する- アロー関数を作成する(これ自体にバイトコードがあります)
- 初期値
0を読み込む - 正しい引数数で呼び出しを設定する
- 実際に呼び出しを実行する
- 結果を
sumに保存する
これらの各ステップは、独自のメタデータを持つ個別のバイトコード命令です。
定数プールはすべてを保存します: すべての文字列リテラル、数値、プロパティ名 - すべてが定数プールに保存されます。ソースコードに "hello" が 100 回あっても、定数プールは 1 回保存しますが、識別子テーブルと定数参照がオーバーヘッドを追加します。
関数ごとのメタデータ: 各関数 - 小さな 1 行の関数でさえ - 完全なメタデータを取得します:
- レジスタ割り当て情報
- コード機能ビットマスク
- 解析モード
- 例外ハンドラー
- デバッグ用の式情報
1,000 の小さな関数があるファイルには、1,000 セットのメタデータがあります。
プロファイリングデータ構造: プロファイリングデータ自体はまだ入力されていなくても、プロファイリングデータを保持する構造が割り当てられます。これには以下が含まれます:
- 値プロファイルスロット(各操作を流れる型を追跡)
- 配列プロファイルスロット(配列アクセスポターンを追跡)
- 二項算術プロファイルスロット(数学演算の数値型を追跡)
- 単項算術プロファイルスロット
これらは空でもスペースを取ります。
事前計算された制御フロー: ジャンプターゲット、スイッチテーブル、例外ハンドラーの境界はすべて事前計算されて保存されます。これにより実行は高速になりますが、ファイルサイズが増加します。
緩和戦略
圧縮: バイトコードは gzip/brotli で非常に良く圧縮されます(60-70% 圧縮)。反復構造とメタデータは効率的に圧縮されます。
先にミニファイ: バイトコード生成前に --minify を使用すると役立ちます:
- 短い識別子 → より小さな識別子テーブル
- デッドコード削除 → 生成されるバイトコードが少なくなる
- 定数畳み込み → プールの定数が少なくなる
トレードオフ: 2-4 倍大きいファイルと 2-4 倍速い起動をトレードオフしています。CLI の場合、これは通常価値があります。数メガバイトのディスクスペースが問題にならない長実行サーバーの場合、さらに問題ではありません。
バージョニングと移植性
クロスアーキテクチャ移植性:✅
バイトコードはアーキテクチャに依存しません。次のことができます:
- macOS ARM64 でビルドし、Linux x64 にデプロイ
- Linux x64 でビルドし、AWS Lambda ARM64 にデプロイ
- Windows x64 でビルドし、macOS ARM64 にデプロイ
バイトコードには任意のアーキテクチャで機能する抽象命令が含まれています。アーキテクチャ固有の最適化は、キャッシュされたバイトコードではなく、ランタイムでの JIT コンパイル中に発生します。
クロスバージョン移植性:❌
バイトコードは Bun バージョン間で安定していません。その理由は次のとおりです:
バイトコード形式の変更: JavaScriptCore のバイトコード形式は進化します。新しいオペコードが追加され、古いものは削除または変更され、メタデータ構造が変更されます。JavaScriptCore の各バージョンには異なるバイトコード形式があります。
バージョン検証: .jsc ファイルヘッダーのキャッシュバージョンは、JavaScriptCore フレームワークのハッシュです。Bun がバイトコードを読み込むとき:
.jscファイルからキャッシュバージョンを抽出します- 現在の JavaScriptCore バージョンを計算します
- 一致しない場合、バイトコードはサイレントに拒否されます
- Bun は
.jsソースコードの解析にフォールバックします
アプリケーションは引き続き実行されます - パフォーマンス最適化が失われるだけです。
優雅な劣化: この設計により、バイトコードキャッシングは「フェイルオープン」します - 何かがうまくいかない場合(バージョンの不一致、破損したファイル、欠落したファイル)、コードは正常に実行されます。起動が遅くなる可能性がありますが、エラーは発生しません。
未リンクとリンクされたバイトコード
JavaScriptCore は「未リンク」と「リンクされた」バイトコードの間に重要な区別をします。この分離がバイトコードキャッシングを可能にします:
未リンクのバイトコード(キャッシュされるもの)
.jsc ファイルに保存されたバイトコードは未リンクのバイトコードです。これには以下が含まれます:
- コンパイルされたバイトコード命令
- コードの構造情報
- 定数と識別子
- 制御フロー情報
しかし、以下は含まれません:
- 実際のランタイムオブジェクトへのポインター
- JIT コンパイルされたマシンコード
- 以前の実行からのプロファイリングデータ
- 呼び出しリンク情報(どの関数がどれを呼び出すか)
未リンクのバイトコードは不変で共有可能です。同じコードの複数の実行はすべて同じ未リンクのバイトコードを参照できます。
リンクされたバイトコード(ランタイム実行)
Bun がバイトコードを実行するとき、それは「リンク」されます - 次のものを追加するランタイムラッパーを作成します:
- 呼び出しリンク情報:コードが実行されると、エンジンはどの関数がどれを呼び出すかを学習し、それらの呼び出しサイトを最適化します。
- プロファイリングデータ:エンジンは各命令が何回実行されるか、コードを流れる値の型、配列アクセスポターンなどを追跡します。
- JIT コンパイル状態:ホットコードのベースライン JIT または最適化 JIT(DFG/FTL)コンパイルバージョンへの参照。
- ランタイムオブジェクト:実際の JavaScript オブジェクト、プロトタイプ、スコープなどへのポインター。
このリンクされた表現は、コードを実行するたびに新しく作成されます。これにより、次のことが可能になります:
- 高コストの作業をキャッシュする(未リンクのバイトコードへの解析とコンパイル)
- 最適化をガイドするためのランタイムプロファイリングデータを収集する
- 実際の実行パターンに基づいて JIT 最適化を適用する
バイトコードキャッシングは、高コストの作業(バイトコードへの解析とコンパイル)をランタイムからビルド時間に移動します。頻繁に起動するアプリケーションの場合、これはディスク上のファイルが大きくなる代わりに、起動時間を半分にすることができます。
本番 CLI とサーバーレスデプロイメントの場合、--bytecode --minify --sourcemap の組み合わせが、デバッグ可能性を維持しながら最良のパフォーマンスを提供します。