Skip to content

Performance

A warm resolve is one Map.get(key) followed by a direct new Ctor(...) — there is no reflection, no metadata table, and no proxy in the way. The benchmark numbers below follow from a few concrete runtime choices, not from a special fast mode you have to opt into:

Runtime choiceEffect
Explicit registrationsContainer build is a flat Map.set per service. There are no decorator side effects, constructor-name parsers, or metadata tables to prepare.
Cached singleton and scoped servicesA warm resolve reads from cache.get(key) before cycle and lifetime bookkeeping runs. The cache.has(key) fallback exists only for explicit undefined values.
Direct constructor callsClasses with 0-7 dependencies use a direct new Ctor(...) path. Larger constructors fall back to Reflect.construct.
Async factoriesThe factory's Promise is cached verbatim, so concurrent callers share one in-flight initialization while .get() stays synchronous.
Strict mode boundarystrict: true catches cycles and lifetime leaks. strict: false removes that bookkeeping for audited hot transient graphs.

Benchmark results

Benchmark Suite

The repository benchmark suite compares InferDI with InversifyJS v8, Awilix v13 in PROXY and CLASSIC modes, TSyringe v4, TypeDI v0.10, and Typed Inject v5.

All numbers are operations per second on Node 22. Higher is better.

ScenarioInferDITyped InjectAwilix (PROXY)Awilix (CLASSIC)InversifyJSTSyringeTypeDI
1. Hot singleton resolve (warm cache)14.2 M7.0 M7.2 M6.9 M6.3 M6.2 M6.4 M
2. Transient resolve (new instance per call)8.4 M4.3 M3.4 M2.9 M3.4 M2.4 M1.6 M
3. Deep graph (10 levels, all transient)1.85 M1.28 M701 k739 k750 k601 k214 k
4a. Wide graph (4 deps, root transient)7.3 M3.2 M2.2 M2.3 M2.3 M1.6 M1.1 M
4b. Wide graph (10 deps, root transient)3.5 M2.6 M1.2 M1.3 M1.6 M1.0 M437 k
5. Container build + first resolve400 k228 k10 k8 k13 k202 k272 k
6. Scoped lifecycle (create + resolve + cleanup)2.66 M2.39 M492 k413 k28 k1.08 M637 k
7. Lazy resolve (deferred wrapper)12.1 M7.0 M5.5 M4.7 M4.2 M4.0 M2.8 M

What the Numbers Show

  • Cached singleton resolve runs about 2x faster than the closest baseline in this suite.
  • Container build plus first resolve favors flat registration. InferDI registers the graph from scratch; decorator-based libraries have already paid part of their registration work during module evaluation.
  • Wide graph scenarios show why arity unrolling matters. Up to seven dependencies, V8 can inline the direct constructor call. At ten dependencies, InferDI falls back to Reflect.construct and still leads the listed baselines.
  • Scoped lifecycle includes scope creation, resolve, and cleanup. Scenario 6 includes disposal work on every iteration, so it measures scope ownership rather than resolve alone.
  • Typed Inject is the closest non-InferDI baseline. Its compile-time-known graph keeps it competitive on deep graphs and scoped flows.

Fast Mode

new Container({ strict: false }) removes runtime cycle bookkeeping, singleton-stack tracking, and the try/finally around the guarded resolve path. The package README reports about 30% faster local transient resolves on a flat transient graph. Cached singleton and scoped resolves do not change, because they return before those guards run.

Use fast mode only after tests have exercised the graph in default strict mode. TypeScript cannot see singleton cycles, transient cycles, dynamic keys, as-casts, or factories that close over a wider outer container.

Small Hot-Path Details

Symbol keys can help in tight resolve loops because Map compares them by identity. String keys need hashing and, on collision, character comparison. Most applications will not measure a difference, so treat symbol keys as a profiler-driven change.

Reproduce Locally

bash
cd benchmarks
pnpm install --frozen-lockfile
pnpm run precondition
pnpm run bench

The benchmark workspace is intentionally isolated from the root pnpm workspace and has its own lockfile. See benchmarks/README.md for methodology, fairness notes, and fixture sources.