Skip to content

バイトコードキャッシングは、JavaScript をバイトコードに事前コンパイルすることでアプリケーションの起動時間を大幅に改善するビルド時最適化です。例えば、バイトコードを有効にして TypeScript の tsc をコンパイルすると、起動時間が2 倍改善されます。

使用方法

基本的な使用方法

--bytecode フラグでバイトコードキャッシングを有効にします:

bash
bun build ./index.ts --target=bun --bytecode --outdir=./dist

これにより、2 つのファイルが生成されます:

  • dist/index.js - バンドルされた JavaScript
  • dist/index.jsc - バイトコードキャッシュファイル

ランタイムでは、Bun は自動的に .jsc ファイルを検出して使用します:

bash
bun ./dist/index.js  # 自動的に index.jsc を使用

スタンドアロン実行可能ファイルの場合

--compile で実行可能ファイルを作成する際、バイトコードはバイナリに埋め込まれます:

bash
bun build ./cli.ts --compile --bytecode --outfile=mycli

結果の実行可能ファイルにはコードとバイトコードの両方が含まれており、単一ファイルで最大のパフォーマンスを実現します。

他の最適化との組み合わせ

バイトコードはミニファイやソースマップと併用できます:

bash
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 は例外です:

js
// これによりバイトコードキャッシングが防止されます
const data = await fetch("https://api.example.com");
export default data;

理由:トップレベル await は非同期モジュール評価を必要とし、これは CommonJS で表現できません。モジュールグラフは非同期になり、CommonJS ラッパー関数モデルは機能しなくなります。

回避策:非同期初期化を関数内に移動します:

js
async function init() {
  const data = await fetch("https://api.example.com");
  return data;
}

export default init;

これで、モジュールはコンシューマーが必要に応じて await できる関数をエクスポートします。

バージョン互換性

バイトコードは Bun バージョン間で移植できません。バイトコード形式は JavaScriptCore の内部表現に紐付いており、バージョン間で変更されます。

Bun を更新したら、バイトコードを再生成する必要があります:

bash
# Bun 更新後
bun build --bytecode ./index.ts --outdir=./dist

バイトコードが現在の Bun バージョンと一致しない場合、自動的に無視され、コードは JavaScript ソースの解析にフォールバックします。アプリは実行されますが、パフォーマンス最適化は失われます。

ベストプラクティス:CI/CD ビルドプロセスの一部としてバイトコードを生成します。.jsc ファイルを git にコミットしないでください。Bun を更新するたびに再生成します。

ソースコードは依然として必要

  • .js ファイル(バンドルされたソースコード)
  • .jsc ファイル(バイトコードキャッシュ)

ランタイムでは:

  1. Bun は .js ファイルを読み込み、@bytecode プラグマを確認し、.jsc ファイルをチェックします
  2. Bun は .jsc ファイルを読み込みます
  3. Bun はバイトコードハッシュがソースと一致することを検証します
  4. 有効な場合、Bun はバイトコードを使用します
  5. 無効な場合、Bun はソースの解析にフォールバックします

バイトコードは難読化ではありません

バイトコードはソースコードを難読化しません。これは最適化であり、セキュリティ対策ではありません。

本番環境デプロイ

Docker

Dockerfile にバイトコード生成を含めます:

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

ビルドパイプライン中にバイトコードを生成します:

yaml
# GitHub Actions
- name: Build with bytecode
  run: |
    bun install
    bun build --bytecode --minify \
      --outdir=./dist \
      --target=bun \
      ./src/index.ts

デバッグ

バイトコードが使用されていることを確認

.jsc ファイルが存在することを確認します:

bash
ls -lh dist/
txt
-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 を設定します。

成功すると、次のようなログが出力されます:

txt
[Disk cache] cache hit for sourceCode

キャッシュミスがある場合は、次のようなログが出力されます:

txt
[Disk cache] cache miss for sourceCode

Bun は現在組み込みモジュールで使用される JavaScript コードをバイトコードキャッシュしないため、キャッシュミスが複数回ログ出力されるのは正常です。

一般的な問題

バイトコードがサイレントに無視される:通常は Bun バージョンの更新が原因です。キャッシュバージョンが一致しないため、バイトコードが拒否されます。再生成して修正します。

ファイルサイズが大きすぎる:これは予想通りです。次のことを検討してください:

  • バイトコード生成前に --minify を使用してコードサイズを削減する
  • ネットワーク転送用に .jsc ファイルを圧縮する(gzip/brotli)
  • 起動パフォーマンスの改善がサイズ増加に見合う価値があるかを評価する

トップレベル await:サポートされていません。非同期初期化関数を使用するようにリファクタリングします。

バイトコードとは

JavaScript を実行するとき、JavaScript エンジンはソースコードを直接実行するわけではありません。代わりに、いくつかのステップを踏みます:

  1. 解析:エンジンは JavaScript ソースコードを読み取り、抽象構文木(AST)に変換します
  2. バイトコードコンパイル:AST はバイトコードにコンパイルされます - これは実行がより高速な低レベル表現です
  3. 実行:バイトコードはエンジンのインタープリターまたは JIT コンパイラーによって実行されます

バイトコードは中間表現です - JavaScript ソースコードよりも低レベルですが、マシンコードよりも高レベルです。仮想マシンのアセンブリ言語のようなものだと考えてください。各バイトコード命令は、「この変数を読み込む」「2 つの数を加算する」「この関数を呼び出す」などの単一操作を表します。

これはコードを実行するたびに発生します。1 日に 100 回実行される CLI ツールがある場合、コードは 100 回解析されます。頻繁なコールドスタートを伴うサーバーレス関数がある場合、コールドスタートごとに解析が発生します。

バイトコードキャッシングを使用すると、Bun はステップ 1 と 2 をビルドステップに移動します。ランタイムでは、エンジンは事前コンパイルされたバイトコードを読み込み、実行に直接移行します。

レイジー解析がこれをさらに良くする理由

モダンな JavaScript エンジンはレイジー解析と呼ばれる巧妙な最適化を使用します。すべてのコードを事前に解析するのではなく、関数は初めて呼び出されたときのみ解析されます:

js
// バイトコードキャッシングなし:
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)として保存されるため、ランタイムでソースから解析する必要がありません。
  • 識別子テーブル:コードで使用されるすべての変数と関数名。重複除外された文字列として保存されます。
  • ソースコード表現マーカー:定数をどのように表現するか(整数、ダブル、ビッグイントなど)を示すフラグ。

関数メタデータ(コード内の各関数):

  • レジスタ割り当て:関数が必要とするレジスタ(ローカル変数)の数 - thisRegisterscopeRegisternumVarsnumCalleeLocalsnumParameters
  • コード機能:コンストラクターか?アロー関数か?super を使用するか?テールコールがあるか?などの関数の特性のビットマスク。これらは関数の実行方法に影響します。
  • 語彙スコープ機能:厳格モードなどの語彙コンテキスト。
  • 解析モード:関数が解析されたモード(通常、非同期、ジェネレーター、非同期ジェネレーター)。

ネストされた構造

  • 関数宣言と式:各ネストされた関数は再帰的に独自のバイトコードブロックを取得します。100 の関数があるファイルには、構造内にネストされた 100 の個別のバイトコードブロックがあります。
  • 例外ハンドラー:境界とハンドラーアドレスが事前計算された try/catch/finally ブロック。
  • 式情報:エラー報告とデバッグのためにバイトコード位置をソースコード位置にマッピングします。

バイトコードに含まれていないもの

重要なこととして、バイトコードにはソースコードは埋め込まれていません。代わりに:

  • JavaScript ソースは別々に(.js ファイルに)保存されます
  • バイトコードはソースのハッシュと長さのみを保存します
  • 読み込み時に、Bun はバイトコードが現在のソースコードと一致することを検証します

これが、.js ファイルと .jsc ファイルの両方をデプロイする必要がある理由です。.jsc ファイルは対応する .js ファイルなしでは役に立ちません。

トレードオフ:ファイルサイズ

バイトコードファイルはソースコードよりも大幅に大きくなります - 通常 2-8 倍大きくなります。

バイトコードがはるかに大きい理由

バイトコード命令は冗長です: ミニファイされた JavaScript の 1 行は、数十のバイトコード命令にコンパイルされる可能性があります。例えば:

js
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 がバイトコードを読み込むとき:

  1. .jsc ファイルからキャッシュバージョンを抽出します
  2. 現在の JavaScriptCore バージョンを計算します
  3. 一致しない場合、バイトコードはサイレントに拒否されます
  4. Bun は .js ソースコードの解析にフォールバックします

アプリケーションは引き続き実行されます - パフォーマンス最適化が失われるだけです。

優雅な劣化: この設計により、バイトコードキャッシングは「フェイルオープン」します - 何かがうまくいかない場合(バージョンの不一致、破損したファイル、欠落したファイル)、コードは正常に実行されます。起動が遅くなる可能性がありますが、エラーは発生しません。

未リンクとリンクされたバイトコード

JavaScriptCore は「未リンク」と「リンクされた」バイトコードの間に重要な区別をします。この分離がバイトコードキャッシングを可能にします:

未リンクのバイトコード(キャッシュされるもの)

.jsc ファイルに保存されたバイトコードは未リンクのバイトコードです。これには以下が含まれます:

  • コンパイルされたバイトコード命令
  • コードの構造情報
  • 定数と識別子
  • 制御フロー情報

しかし、以下は含まれません

  • 実際のランタイムオブジェクトへのポインター
  • JIT コンパイルされたマシンコード
  • 以前の実行からのプロファイリングデータ
  • 呼び出しリンク情報(どの関数がどれを呼び出すか)

未リンクのバイトコードは不変で共有可能です。同じコードの複数の実行はすべて同じ未リンクのバイトコードを参照できます。

リンクされたバイトコード(ランタイム実行)

Bun がバイトコードを実行するとき、それは「リンク」されます - 次のものを追加するランタイムラッパーを作成します:

  • 呼び出しリンク情報:コードが実行されると、エンジンはどの関数がどれを呼び出すかを学習し、それらの呼び出しサイトを最適化します。
  • プロファイリングデータ:エンジンは各命令が何回実行されるか、コードを流れる値の型、配列アクセスポターンなどを追跡します。
  • JIT コンパイル状態:ホットコードのベースライン JIT または最適化 JIT(DFG/FTL)コンパイルバージョンへの参照。
  • ランタイムオブジェクト:実際の JavaScript オブジェクト、プロトタイプ、スコープなどへのポインター。

このリンクされた表現は、コードを実行するたびに新しく作成されます。これにより、次のことが可能になります:

  1. 高コストの作業をキャッシュする(未リンクのバイトコードへの解析とコンパイル)
  2. 最適化をガイドするためのランタイムプロファイリングデータを収集する
  3. 実際の実行パターンに基づいて JIT 最適化を適用する

バイトコードキャッシングは、高コストの作業(バイトコードへの解析とコンパイル)をランタイムからビルド時間に移動します。頻繁に起動するアプリケーションの場合、これはディスク上のファイルが大きくなる代わりに、起動時間を半分にすることができます。

本番 CLI とサーバーレスデプロイメントの場合、--bytecode --minify --sourcemap の組み合わせが、デバッグ可能性を維持しながら最良のパフォーマンスを提供します。

Bun by www.bunjs.com.cn 編集