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 and walks the exact parent chain so tree mutations remain immediately visible. strict: false trusts an audited, immutable production graph.

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. Fast scopes read the immutable root registry directly instead of walking the parent chain, then mirror delegated singletons into the scope cache. Strict scopes walk their exact parent chain on every local miss, keeping mutations observable without per-scope lookup metadata or invalidation bookkeeping. Registration skips defensive invalidation in Fast Mode. Owned-instance identity de-duplication runs once during disposal in both modes rather than scanning the queue during creation.

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. Register each runtime key once through one linear fluent chain, complete registration before the first resolve or scope, keep the activated tree immutable, and dispose child scopes before their ancestors.

For a profiled production path, { strict: false } is the fastest supported configuration after that verification. Keep strict mode for development, tests, hot reload, and any tree that is mutated after activation.

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.