Skip to content

Performance

A warm resolve reads Map.get(key) and calls new Ctor(...) directly when construction is required. The benchmark numbers below reflect concrete runtime choices:

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.

ScenarioInferDIInversifyJSTyped InjectAwilix (PROXY)Awilix (CLASSIC)TSyringeTypeDI
1. Hot singleton resolve (warm cache)14.3 M10.7 M7.0 M7.3 M6.7 M5.8 M6.45 M
2. Transient resolve (new instance per call)9.75 M6.1 M4.1 M3.45 M3.0 M2.5 M1.6 M
3. Deep graph (10 levels, all transient)2.3 M1.5 M1.3 M716 k736 k643 k222 k
4a. Wide graph (4 deps, root transient)8.25 M4.9 M3.4 M2.2 M2.3 M1.65 M1.1 M
4b. Wide graph (10 deps, root transient)3.5 M1.9 M2.6 M1.2 M1.3 M938 k458 k
5. Container build + first resolve400 k13.2 k223 k10 k8.3 k206 k282 k
6. Scoped lifecycle (create + resolve + cleanup)2.85 M35 k2.45 M330 k430 k1.1 M665 k
7. Lazy resolve (deferred wrapper)11.8 M7.6 M7.15 M5.6 M4.7 M4.25 M2.85 M

What the Numbers Show

  • Cached singleton resolve is 1.34x faster than the closest baseline, InversifyJS.
  • 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. With four dependencies, InferDI leads the closest baseline by 1.68x. At ten dependencies, it falls back to Reflect.construct and remains 1.35x ahead of Typed Inject.
  • 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.
  • InferDI leads all 8 scenarios. Typed Inject is the closest non-InferDI baseline on scoped flows and the 10-dependency wide graph, while InversifyJS is closest on cached singleton, transient, deep-graph, and four-dependency-wide resolves.

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.

For a profiled, transient-heavy production path, { strict: false } is the fastest supported configuration after that verification. Keep strict mode for development and test runs. It does not improve cached singleton or scoped resolves.

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.