Skip to content

パフォーマンス

記録された結果

2026-08-17 の公開計測では、既定の checked モードでホットな singleton を解決した中央値は 6.233 ns/op でした。この 1 シナリオと記録されたパッケージバージョンに限ると、参加コンテナーの中央値はその 1.76 倍から 13.05 倍 でした。これはコンテナーのオーバーヘッドであり、HTTP スループットやアプリケーション全体の性能ではありません。raw result には全 8 ラウンドと環境メタデータが含まれます。

ウォームな解決は Map.get(key) を読み取り、構築が必要な場合は new Ctor(...) を直接呼び出します。ベンチマークシナリオは、次のランタイム上の選択を対象にします。

ランタイム上の選択効果
明示的な登録コンテナのビルドはサービスごとのフラットな Map.set です。準備すべきデコレーターの副作用も、コンストラクター名のパーサーも、メタデータテーブルもありません。
シングルトンおよびスコープドサービスのキャッシュウォームな解決は、サイクルとライフタイムの管理処理が実行される前に cache.get(key) から読み取ります。明示的な undefined は内部の UNDEFINED_MARKER として保存されるため、キャッシュヒットは 1 回の検索で済みます。
直接的なコンストラクター呼び出し依存が 0〜7 個のクラスは直接的な new Ctor(...) の経路を使用します。より大きなコンストラクターは Reflect.construct にフォールバックします。
非同期ファクトリーファクトリーの Promise はそのままキャッシュされるため、.get() が同期のままで、並行する呼び出し元は進行中の初期化を 1 つ共有します。
ランタイム契約デフォルト/fast: false はランタイムチェックと変更可能な正確な親チェーンを維持します。fast: true はチェックを外し、固定トポロジーの scope lookup を有効にします。

ベンチマークスイート

比較ワークスペースでは、InferDI (fast)InferDI (default) を、InversifyJS v8、PROXY および CLASSIC モードの Awilix v13、TSyringe v4、TypeDI v0.10、Typed Inject v5 と同じスイートで計測します。raw result には、この実行で使用したパッケージバージョンとマシン環境が記録されています。

計測前の正当性検証

アダプター契約テストは Tinybench より先に実行されます。各アダプターは同じ観測可能なグラフを提供する必要があります。

  • LoggerConfigRepoService はルートシングルトンとして動作する
  • transient グラフのノードは新しいインスタンスを生成する
  • 各 scope は独自の ScopedService をキャッシュし、ルート logger を共有する
  • lazy アクセスは対象サービスの解決を遅延させる
  • 宣言された各 teardown API は scoped サービスの disposer を呼び出す

この検証により、ライフタイムやインスタンス同一性の違いが計測比較に混入するのを防ぎます。タイミングシナリオは、アダプターが契約を満たした後のコストを測ります。

計測設計

公開ランナーは frozen lockfile で依存関係をインストールし、ベンチマークワークスペースを型チェックし、InferDI の本番 ESM アーティファクトをビルドして、そのアーティファクトに対して契約テストを実行します。その後、次の条件で各 subject を計測します。

  1. 8 個の subject に対応する 8 ラウンドで、1 つの均衡ラテン方格ブロックを構成します。
  2. 各 subject はすべてのプロセス起動位置に 1 回ずつ配置され、ほかの各 subject の直後にも 1 回ずつ配置されます。InferDI (fast) は 4 ラウンドで InferDI (default) より先に、残りの 4 ラウンドで後に実行されます。
  3. 各 subject は新しい Node プロセスで実行されるため、実装間で JIT フィードバック、インラインキャッシュ、GC 履歴を共有しません。
  4. Tinybench は各シナリオを 50 ms ウォームアップし、100 ms 計測します。1 サンプルは、その操作に適したサイズのバッチを実行します。
  5. subject、シナリオ、ラウンドの各組み合わせについて、ランナーは Tinybench のバッチ時間の中央値をバッチサイズで割り、正規化した ns/op を記録します。
  6. レポーターは 8 ラウンドの値を中央値と中央絶対偏差(MAD)で集約します。

ns/op は小さいほど高速です。MAD は中央値周辺のばらつきを示しますが、信頼区間ではありません。プロセス分離と均衡した起動順序は計測バイアスを減らしますが、OS スケジューリング、CPU 周波数の変化、JIT の判断、GC のタイミングを完全には除去できません。

シナリオの境界

各シナリオはコンテナ処理の一段階を分離します。プロダクションコードはライフタイムモデルに応じて、これらの段階を組み合わせます。

ワークロード群シナリオ対応するコンテナ処理
ウォームアクセスホットシングルトン解決、ウォーム scoped 解決、lazy 解決キャッシュ済みサービスの読み取り、または既存の lazy ラッパー経由のアクセス
オブジェクト生成transient 解決、深いグラフ、幅広いグラフキャッシュミス、依存関係の走査、引数の組み立て、コンストラクター呼び出し
起動とコールドアクセス登録、最初の解決グラフ設定と最初のキャッシュ生成
Scope のライフサイクルScope 作成、最初の scoped 解決、同期 teardown、非同期 teardownリクエストまたはジョブごとの scope 所有を作成からクリーンアップまで計測

Tinybench の setup hook は計測区間の外でコールドグラフと scope を準備します。登録と解決シナリオのクリーンアップも計測外です。Teardown シナリオでは、クリーンアップ自体を対象として計測します。

TypeDI の Registration は N/A です。同等のサービス定義がモジュール評価時にデコレーターの副作用として実行され、登録タイマーがその処理を含まないためです。Teardown 行には同等の公開契約を持つライブラリだけが参加します。InversifyJS は N/A、TypeDI は同期 teardown、Awilix、TSyringe、Typed Inject は非同期 teardown に参加します。InferDI は両方のクリーンアップ契約を提供します。

公開結果

各セルは ns/op 単位の 中央値 ± MAD です。括弧内の係数は、その中央値を同じ行の最小中央値と比較しています。

ベンチマーク結果

シナリオInferDI (fast)InferDI (default)InversifyJSAwilix PROXYAwilix CLASSICTSyringeTypeDITyped Inject
ホットシングルトン解決6.233 ± 0.091 (1.00×)6.233 ± 0.000 (1.00×)10.954 ± 0.321 (1.76×)40.425 ± 0.596 (6.49×)41.525 ± 0.367 (6.66×)81.354 ± 3.254 (13.05×)71.729 ± 0.962 (11.51×)48.354 ± 1.374 (7.76×)
Transient 解決41.798 ± 2.196 (1.00×)45.834 ± 1.284 (1.10×)79.566 ± 3.850 (1.90×)219.82 ± 8.250 (5.26×)227.15 ± 8.068 (5.43×)301.40 ± 16.682 (7.21×)519.75 ± 5.498 (12.43×)138.05 ± 1.464 (3.30×)
深いグラフ(10 レベル)344.21 ± 11.460 (1.00×)445.50 ± 10.085 (1.29×)397.37 ± 7.795 (1.15×)1306.3 ± 16.500 (3.79×)1190.8 ± 12.375 (3.46×)1463.0 ± 34.835 (4.25×)4478.8 ± 26.582 (13.01×)717.75 ± 3.670 (2.09×)
幅広いグラフ(4 依存)59.584 ± 1.282 (1.00×)71.316 ± 1.284 (1.20×)103.22 ± 2.932 (1.73×)331.10 ± 6.416 (5.56×)302.69 ± 5.498 (5.08×)457.97 ± 18.150 (7.69×)863.32 ± 12.832 (14.49×)197.63 ± 1.468 (3.32×)
幅広いグラフ(10 依存)188.37 ± 4.125 (1.00×)200.74 ± 4.130 (1.07×)394.63 ± 4.580 (2.09×)681.08 ± 21.080 (3.62×)571.54 ± 12.830 (3.03×)979.46 ± 15.125 (5.20×)2234.1 ± 25.888 (11.86×)321.29 ± 4.585 (1.71×)
登録2007.5 ± 27.500 (1.00×)2158.8 ± 13.750 (1.08×)61274.6 ± 1537.7 (30.52×)74763.4 ± 1024.4 (37.24×)95225.6 ± 467.45 (47.43×)3762.9 ± 36.650 (1.87×)N/A3593.3 ± 18.300 (1.79×)
最初の解決653.13 ± 24.745 (1.00×)794.98 ± 6.190 (1.22×)9967.4 ± 551.61 (15.26×)2434.9 ± 96.250 (3.73×)3044.2 ± 70.360 (4.66×)1236.8 ± 25.440 (1.89×)3322.9 ± 22.455 (5.09×)1275.3 ± 22.917 (1.95×)
Scope 作成79.750 ± 2.290 (1.00×)83.415 ± 5.505 (1.05×)6194.4 ± 167.98 (77.67×)1054.4 ± 16.960 (13.22×)1031.2 ± 7.790 (12.93×)503.26 ± 10.540 (6.31×)419.38 ± 3.210 (5.26×)164.08 ± 1.835 (2.06×)
最初の scoped 解決115.05 ± 1.830 (1.00×)128.34 ± 1.835 (1.12×)3048.6 ± 32.765 (26.50×)352.92 ± 1.835 (3.07×)362.54 ± 4.590 (3.15×)373.54 ± 11.915 (3.25×)296.08 ± 4.125 (2.57×)203.05 ± 0.920 (1.76×)
ウォーム scoped 解決8.387 ± 0.092 (1.05×)8.250 ± 0.046 (1.03×)30.204 ± 0.367 (3.79×)90.841 ± 1.421 (11.39×)95.242 ± 1.421 (11.94×)78.375 ± 2.429 (9.83×)92.630 ± 0.458 (11.61×)7.975 ± 0.092 (1.00×)
同期 teardown99.455 ± 1.370 (1.01×)98.085 ± 0.925 (1.00×)N/AN/AN/AN/A124.21 ± 3.665 (1.27×)N/A
非同期 teardown242.91 ± 3.210 (1.00×)249.34 ± 1.840 (1.03×)N/A860.52 ± 20.170 (3.54×)886.87 ± 13.750 (3.65×)1311.8 ± 10.080 (5.40×)N/A721.42 ± 19.935 (2.97×)
Lazy 解決18.837 ± 0.458 (1.00×)19.020 ± 0.274 (1.01×)64.212 ± 3.621 (3.41×)115.68 ± 0.825 (6.14×)124.09 ± 3.231 (6.59×)155.01 ± 5.271 (8.23×)271.06 ± 3.941 (14.39×)76.496 ± 0.962 (4.06×)

ソースデータ

グラフと表は public-2026-08-17T16-46-00-483Z.json のデータを使用しています。raw result には、8 ラウンド、正規化した計測値、Tinybench の統計、環境メタデータ、依存関係のバージョンが含まれます。ベンチマークワークスペースの README では、各シナリオ、対応するプロダクションケース、レポートの計算方法を説明しています。

結果の読み方

13 シナリオ中 12 シナリオで、いずれかの InferDI モードが最小または同率最小の中央値を記録しています。InferDI (fast) は 11 シナリオで最小または同率最小です。登録、最初の解決、transient とグラフの生成、scope 作成、最初の scoped 解決で差が明確に表れています。これらの操作では、InferDI のフラットなレジストリ、直接的なコンストラクター呼び出し、小さなキャッシュミス経路が計測対象の処理に影響します。

Warm scoped resolve は、どの InferDI モードも最小中央値を取らない唯一のシナリオです。Typed Inject は 7.975 ns、InferDI default は 8.250 ns、InferDI fast は 8.387 ns を記録しました。この行は、既存の scope からキャッシュ済みの値を 1 回読み取る処理を計測します。同じプロダクションリクエストが別途負担する可能性がある scope 作成、最初の scoped キャッシュミス、teardown は含みません。

fast がすべての行で小さくならない理由

fast: true は、ガード付きのキャッシュミス、オブジェクト生成、登録時の無効化、固定 scope ツリー内の親検索を変更します。ウォームアップ後にこれらの分岐を実行しないシナリオもあります。

  • ホットシングルトン解決は、InferDI が fast フラグを読む前に cache.get(key) から値を返します。両モードの中央値は同じ 6.233 ns です。
  • ウォーム scoped 解決も同じキャッシュヒット経路を使います。Fast は 8.387 ns、default は 8.250 ns で、同じ実装経路に 0.137 ns の差があります。この差はレポートされた MAD と同じ桁です。
  • 同期 teardown は両モードで同じ disposal 実装を使います。この実行での中央値差 1.370 ns は、fast の MAD と一致します。
  • 非同期 teardown と lazy 解決では fast がわずかに小さいものの、定常状態の処理は解決ガードの無効化から直接の効果を受けません。これらの小さな差も同じ注意をもって解釈する必要があります。

独立した Node プロセスでは、同一のコード経路に異なる JIT コードが生成されたり、スケジューラーや GC のタイミングが異なったりする可能性があります。ラテン方格順序と 8 ラウンドの中央値はその影響を減らしますが、除去はできません。ウォーム scoped 解決と同期 teardown の小さな逆転だけでは、fast モードの回帰を示せません。1 ns 未満や数パーセントの差を判断するには、制御したマシンでの反復実行がより強い根拠になります。

実装経路が変わる場面では、fast と default の差が大きくなります。default の中央値は、10 レベルの transient グラフで fast の 1.29×、最初の解決で 1.22×、4 依存の幅広いグラフで 1.20×、最初の scoped 解決で 1.12× です。この方向は、サイクルとライフタイムの管理を省き、固定トポロジーの scope lookup を使う実装と一致します。

シナリオをアプリケーションに合わせる

アプリケーションが負担するコンテナ処理の組み合わせは異なるため、レポーターは総合スコアを計算しません。長時間動作するプロセスでは、登録は 1 回だけで、その後はウォームなシングルトン読み取りが大半を占める場合があります。HTTP アダプターでは、各リクエストで scope を作成し、scoped グラフを 1 回解決し、キャッシュ済み scoped 値を読み取り、scope を破棄する場合があります。Worker は scope を開かずに transient グラフを生成することもあります。

行をまたいで相対係数を平均しないでください。各シナリオは異なるバッチサイズを使い、独立した計測区間を分離しています。アプリケーションで計測した解決パターンに合う行を選び、その後にフレームワークと I/O を含むアプリケーション全体をプロファイルしてください。

この比較は、記録されたパッケージバージョン、アダプター、fixture、マシンに適用されます。各ライブラリは独自の公開ライフタイムモデルを維持しているため、N/A はその行に同等の操作がないことを示します。このベンチマークが測るのはコンテナのオーバーヘッドであり、エンドツーエンドのリクエスト遅延ではありません。

fast: true

コンストラクター引数のリファレンスはコンテナオプション を参照してください。このセクションでは、その選択がパフォーマンスに与える影響を説明します。

new Container({ fast: true }) は、ランタイムのサイクル管理、シングルトンスタックの追跡、そしてガードされた解決経路を囲む try/finally を取り除きます。固定 scope は registry owner を直接参照して親チェーンの走査を避け、委譲された singleton を scope のキャッシュへ反映します。デフォルトの scope はローカルミスのたびに正確な親チェーンを走査するため、変更が反映されます。fast コンテナは登録時の防御的な無効化を省略します。owned インスタンスの同一性による重複排除は disposal 時に実行されます。

デフォルトの new Container() と明示的な {fast: false} は、ランタイムの安全性チェックと可変グラフを維持します。

fast: true は、fast: false でテストがグラフを十分に実行した後にのみ使用してください。TypeScript はシングルトンのサイクル、トランジェントのサイクル、動的キー、as キャスト、あるいはより広い外側のコンテナをクロージャに取り込むファクトリーを見ることができません。単一の線形 fluent チェーンで各ランタイムキーを一度だけ登録し、最初の解決または scope 作成前に登録を完了し、起動後のツリーを不変に保ち、祖先より先に子 scope を破棄してください。

その検証後、プロファイリング済みのプロダクショングラフでは、{fast: true} により登録、キャッシュミス、グラフ生成、固定 scope lookup のコストを削減できる場合があります。ウォームなキャッシュヒットと disposal は共通経路を使うため、ランタイム契約を選ぶ前にアプリケーションで支配的な段階を計測してください。開発、テスト、ホットリロード、および起動後に変更されるツリーでは fast: false を維持してください。

ホットパスの細かな詳細

transient サービスの生成

transient サービスには通常 registerClass を使います。同じ依存数の異なる transient クラスを 1 つのグラフから繰り返し解決する処理がボトルネックだと計測できた場合だけ変更します。

この限定的な V8 のホットスポットでは、明示的なファクトリーによりサービスごとの生成呼び出し位置を保てます。

ts
const container = new Container()
  .declareScopeInputs<{ context: RequestContext }>()
  .registerClass('schema', Schema, [])
  .registerFactory(
    'parseRequest',
    (c) => new ParseRequest(c.get('context'), c.get('schema')),
    ['context', 'schema'],
    'transient'
  )

ファクトリーでは依存関係を重複して記述するため、実際のアプリケーションで効果を計測してから使ってください。共通のジェネリック生成ヘルパーを挟むと呼び出し位置が統合され、この最適化は失われます。

キーの表現

シンボルキーは、Map がそれらを同一性で比較するため、タイトな解決ループで役立つことがあります。文字列キーはハッシュ化が必要で、衝突時には文字単位の比較が必要です。ほとんどのアプリケーションでは差を計測できないため、シンボルキーはプロファイラー主導の変更として扱ってください。

ローカルで再現する

bash
cd benchmarks
pnpm install --frozen-lockfile
pnpm run bench:quick   # ローカルのソース確認
pnpm run bench:public  # 本番アーティファクト、subject ごとに新しいプロセス

ベンチマークのワークスペースは意図的にルートの pnpm ワークスペースから分離されており、独自のロックファイルを持っています。方法論、公平性に関する注記、そしてフィクスチャのソースについては benchmarks/README.md を参照してください。