性能
记录结果
在 2026-08-17 记录的公开测试中,默认 checked 模式解析热门单例的中位数为 6.233 ns/op。只针对这一场景和记录中的包版本,其他参测容器的中位数是它的 1.76 到 13.05 倍。这些数字衡量容器开销,不代表 HTTP 吞吐量或整个应用的性能。原始结果包含全部八轮数据和环境元数据。
一次热路径解析会读取 Map.get(key),并在需要构造时直接调用 new Ctor(...)。基准测试场景覆盖以下运行时选择:
| 运行时选择 | 效果 |
|---|---|
| 显式注册 | 容器构建就是为每个服务执行一次扁平的 Map.set。没有装饰器的副作用、构造函数名称解析器,也没有需要预先准备的元数据表。 |
| 缓存的单例和作用域级服务 | 一次热路径解析会先从 cache.get(key) 读取,然后才运行循环检测和生命周期记账。显式 undefined 会存为内部 UNDEFINED_MARKER,所以命中缓存仍只需一次查询。 |
| 直接调用构造函数 | 具有 0-7 个依赖的类使用直接的 new Ctor(...) 路径。更大的构造函数则回退到 Reflect.construct。 |
| 异步工厂 | 工厂返回的 Promise 会被原样缓存,因此并发调用者共享同一个进行中的初始化,而 .get() 仍保持同步。 |
| 运行时契约 | 默认值/fast: false 保留运行时检查和精确的可变父链。fast: true 关闭检查并启用固定拓扑的作用域查找。 |
基准测试套件
对比工作区同时测量 InferDI (fast)、InferDI (default)、InversifyJS v8、PROXY 和 CLASSIC 模式下的 Awilix v13、TSyringe v4、TypeDI v0.10,以及 Typed Inject v5。原始结果记录了本次运行使用的包版本和机器环境。
计时前的正确性验证
适配器契约测试在 Tinybench 之前运行。每个适配器必须提供相同的可观察依赖图:
Logger、Config、Repo和Service是根单例- 瞬态图节点会生成新实例
- 每个 scope 缓存自己的
ScopedService,同时共享根 logger - lazy 访问会推迟目标服务的解析
- 每个声明的 teardown API 都会调用 scoped 服务的 disposer
这些检查防止生命周期或实例身份差异进入计时比较。适配器通过契约测试后,计时场景才开始测量成本。
测量设计
公开运行器使用 frozen lockfile 安装依赖,对基准测试工作区执行类型检查,构建 InferDI 的生产 ESM 产物,并针对该产物运行契约测试。随后按以下条件测量各个测试对象:
- 一个平衡拉丁方区组包含八轮,与八个测试对象对应。
- 每个测试对象在每个进程启动位置出现一次,并在其他每个测试对象之后出现一次。
InferDI (fast)在四轮中先于InferDI (default),在另外四轮中后于它。 - 每个测试对象都在新的 Node 进程中运行,因此不同实现不会共享 JIT 反馈、内联缓存或 GC 历史。
- Tinybench 为每个场景预热 50 ms,再测量 100 ms。每个样本执行一组与该操作成本相适应的批量调用。
- 对于每个测试对象、场景和轮次,运行器把 Tinybench 批次耗时的中位数除以批量大小,记录标准化的
ns/op。 - 报告器使用中位数和中位绝对偏差(MAD)聚合八轮数据。
ns/op 越低越好。MAD 描述中位数周围的离散程度,不是置信区间。进程隔离和平衡启动顺序可以降低测量偏差,但无法消除操作系统调度、CPU 频率变化、JIT 决策和 GC 时机的影响。
场景边界
每个场景只隔离容器工作的一部分。生产代码会根据生命周期模型组合其中若干阶段。
| 工作负载组 | 场景 | 对应的容器工作 |
|---|---|---|
| 热访问 | 热单例解析、热 scoped 解析、lazy 解析 | 读取缓存服务,或通过已有的 lazy 包装器访问服务 |
| 对象构造 | 瞬态解析、深层图、宽图 | 缓存未命中、依赖遍历、参数组装和构造函数调用 |
| 启动与冷访问 | 注册、首次解析 | 配置依赖图并首次填充缓存 |
| 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) | InversifyJS | Awilix PROXY | Awilix CLASSIC | TSyringe | TypeDI | Typed 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×) |
| 瞬态解析 | 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/A | 3593.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×) |
| 同步 teardown | 99.455 ± 1.370 (1.01×) | 98.085 ± 0.925 (1.00×) | N/A | N/A | N/A | N/A | 124.21 ± 3.665 (1.27×) | N/A |
| 异步 teardown | 242.91 ± 3.210 (1.00×) | 249.34 ± 1.840 (1.03×) | N/A | 860.52 ± 20.170 (3.54×) | 886.87 ± 13.750 (3.65×) | 1311.8 ± 10.080 (5.40×) | N/A | 721.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 中的数据。原始结果包含八轮数据、标准化测量值、Tinybench 统计信息、环境元数据和依赖版本。基准测试工作区 README 说明了每个场景、对应的生产用例和报告计算方式。
结果解读
在 13 个场景中的 12 个里,至少一个 InferDI 模式取得最低中位数或并列最低中位数。InferDI (fast) 在 11 个场景中取得最低或并列最低中位数。注册、首次解析、瞬态和依赖图构造、scope 创建以及首次 scoped 解析中的优势最明显。这些操作会进入 InferDI 的扁平注册表、直接构造函数调用和精简缓存未命中路径所影响的代码段。
Warm scoped resolve 是前一统计的例外。Typed Inject 为 7.975 ns,InferDI default 为 8.250 ns,InferDI fast 为 8.387 ns。该行测量从现有 scope 中读取一次缓存值,不包含 scope 创建、首次 scoped 缓存未命中或 teardown,而同一个生产请求可能还会分别承担这些成本。
为什么 fast 没有在每一行都更低
fast: true 会改变受保护的缓存未命中、对象构造、注册失效处理,以及固定 scope 树中的父级查找。预热后,有些场景不会执行这些分支:
- 热单例解析在读取
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 有小幅优势,但其稳态工作也不会从关闭解析 guard 中直接受益。这些小差距也需要谨慎解读。
独立的 Node 进程可能让相同代码路径得到不同的 JIT 代码,或遇到不同的调度器和 GC 时机。拉丁方顺序和八轮中位数会减小这些影响,但无法消除它们。热 scoped 解析和同步 teardown 中的小幅反转不足以证明 fast 模式出现性能回退。对于亚纳秒和低个位数百分比的差距,在受控机器上重复运行能提供更强的证据。
当实现路径发生变化时,fast 与 default 的差距更大。十层瞬态图中,default 中位数是 fast 的 1.29×;首次解析中为 1.22×;四依赖宽图中为 1.20×;首次 scoped 解析中为 1.12×。这些方向与移除循环和生命周期记账以及采用固定拓扑 scope 查找相符。
让场景匹配应用
报告器不计算总分,因为应用会承担不同组合的容器工作。长时间运行的进程可能只注册一次,随后大部分时间读取热单例。HTTP 适配器可能为每个请求创建 scope、解析一次 scoped 图、读取缓存的 scoped 值,然后释放 scope。Worker 也可能只构造瞬态图而不创建 scope。
不要对各行的相对系数求平均值。场景使用不同的批量大小,并隔离不同的计时区间。请选择与应用实测解析模式相符的行,再结合框架和 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: false 充分演练过依赖图之后,才使用 fast: true。TypeScript 无法看到单例循环、瞬态循环、动态键、as 类型断言,或闭包捕获了更外层容器的工厂。请通过单一线性 fluent 链对每个运行时键只注册一次,在首次解析或创建 scope 前完成所有注册,激活后保持依赖树不可变,并先释放子 scope,再释放其祖先。
完成上述验证后,对于经过性能分析的生产依赖图,{fast: true} 可以降低注册、缓存未命中、依赖图构造和固定 scope 查找的成本。热缓存命中和 disposal 使用共同路径,因此应先测量应用中占主要成本的阶段,再选择运行时契约。开发、测试、热重载以及任何激活后仍会变更的依赖树都应保留 fast: false。
热路径的小细节
transient 服务的构造
registerClass 是 transient 服务的默认选择。只有性能分析确认同一依赖图频繁解析许多依赖数量相同的 transient 类时,才需要改用工厂。
针对这一特定的 V8 热点,显式工厂可以为每项服务保留独立的构造调用点:
const container = new Container()
.declareScopeInputs<{ context: RequestContext }>()
.registerClass('schema', Schema, [])
.registerFactory(
'parseRequest',
(c) => new ParseRequest(c.get('context'), c.get('schema')),
['context', 'schema'],
'transient'
)工厂会重复依赖列表,因此只应在实际应用中测得收益后使用。共享的泛型构造辅助函数会合并调用点,使这项优化失效。
键的表示方式
Symbol 键在密集的解析循环中可能有所帮助,因为 Map 按身份比较它们。字符串键需要哈希计算,且在冲突时还需逐字符比较。大多数应用不会测量出差异,因此应将 symbol 键视为由性能剖析驱动的改动。
在本地复现
cd benchmarks
pnpm install --frozen-lockfile
pnpm run bench:quick # 本地源码检查
pnpm run bench:public # 生产产物,每个测试对象使用全新进程基准测试工作区有意与根 pnpm 工作区隔离,并拥有自己的 lockfile。有关方法论、公平性说明和 fixture 来源,请参阅 benchmarks/README.md。
