This page documents the 0.4 release candidates of @nexusdi/core. The newest on npm is 0.4.0-rc.1. The documentation for 0.3.2, the current release, is at nexus.js.org. Try the release candidate.
Understanding Benchmark Measurements
To provide an objective comparison of dependency injection containers, NexusDI uses a rigorous benchmark harness. Every figure is based on the Meridian-8 graph, which combines the DI patterns listed below. The harness is in benchmarks/. Every figure on NexusDI vs Other Containers comes from its results files in benchmarks/results/.
The Meridian-8 Graph
The benchmark builds a graph of eight providers: ReactorCore, ShipComputer, PowerRouter, ShieldGrid, NavCharts, Bridge, SurveyDrone, and FlightLog.
This specific graph is used because it tests several critical DI patterns:
- Deep Chains:
Bridge$\rightarrow$ShieldGrid$\rightarrow$PowerRouter$\rightarrow$ReactorCore. - Diamonds: Multiple paths leading back to
ReactorCore. - Mixed Lifetimes: Includes Singletons, Transients, and Scoped providers.
- Value Providers: Includes non-class providers (like
NavCharts).
Each class carries a kind field with its role name. The scenario script prints those fields and compares them with benchmarks/fixtures/golden.json, so a minified bundle that renames every class prints the same output. The fixtures for the five libraries are in benchmarks/fixtures/<library>/.
The Variants Each Library Is Written In
Each library appears in the variants its own documentation teaches, and each fixture’s header comment cites the documentation page and the date it was read, and lists every departure with its reason. NexusDI has plain (tokens and static deps) and decorated (standard decorators). InversifyJS and tsyringe have plain and two decorated variants: decorated uses legacy decorators with decorator metadata, and decorated-explicit uses legacy decorators with explicit tokens. awilix has plain, and needle-di has plain and decorated. That makes eleven library variants.
benchmarks/libraries.json pins each library’s version and marks one variant per library as the one its documentation teaches. The performance table on the comparison page shows that variant. The same file lists the toolchains each library’s documentation names, and records the reason for every lifetime a library lacks.
Testing Across Toolchains
Because different libraries require different compiler flags (like emitDecoratorMetadata), we test every library across a matrix of toolchains, including:
- Standard TS:
tscandtsgo. - Modern Bundlers: esbuild, SWC, and Vite.
- Further Bundler Cells: Babel, Vite with Oxc and Vite with a Babel plugin.
- Runtimes: Bun, Deno, and Node.js (including its built-in type stripping).
This ensures that our performance figures reflect real-world usage across different build pipelines. The cells live in examples/toolchain-matrix/toolchains.json, which pins every toolchain version. A decorator profile sets the compiler flags per cell: none has no decorators, standard uses TC39 decorators, legacy-metadata sets experimentalDecorators and emitDecoratorMetadata, and legacy sets experimentalDecorators alone. Each toolchain gets the flags its own documentation names.
Each cell runs one section per lifetime and takes the worst section outcome:
pass: The printed output equalsgolden.json.compile-error: The build failed, or the runtime refused the module before the container started.runtime-error: A resolve or the process threw after the module loaded.wrong-instance: The scenario finished and printed a field that differs fromgolden.json, such as a missing dependency or a second instance of a singleton.not-applicable: The library has no such lifetime, with the reason inlibraries.json.
A failing cell keeps its first error line, and a cell whose toolchain documents the limitation links that page. The results are in benchmarks/results/matrix.json.
Measuring Performance
1. Bundle Size
We measure the final output bytes after esbuild minification and Gzip compression. This represents the actual cost to the end-user’s browser or server. Bundle size starts from each variant’s tsc output, and esbuild and Rollup bundle it for the browser. The record keeps the gzip size of the decorator metadata polyfill on its own, and runs every minified bundle through the scenario, because minification renames classes. The results are in benchmarks/results/size.json.
2. Startup and Resolution
- Cold Start: The time it takes to spawn a fresh Node process and initialize the container.
- Resolve Speed: The time for a single transient resolution on a ready container.
- Build Time: The wall time required to compile a 200-class graph.
To reduce noise, we use a balanced Latin square rotation, so no library always runs first or always follows the same neighbour. A bootstrap with 10,000 resamples gives each paired ratio an interval at the 0.95 level, and an interval that contains 1 shows no measured difference.
Here is how each figure is produced:
- Process isolation: Timings run on Node 24, on each variant’s
tscbuild, for the scenarios whose matrix section passes. Each library and variant runs in its own worker process, so heaps and compiled code stay apart. - Scenarios:
cold-start,ready,resolve-singleton,resolve-transientandscope-cycle.readycreates a container and resolves every singleton once, so each library does the same work before the clock stops. - Fixed counts: One hundred warm-up iterations are excluded and one thousand are measured, with no early stop. The cold start spawns a fresh
nodeprocess for each library’s documented variant, with twenty warm-up rounds and one thousand measured. A median below two nanoseconds fails the run, because no container resolves that fast. - Statistics: Each figure is a median, published with its MAD and its 5th to 95th percentile range, and a figure whose MAD exceeds one twentieth of its median carries a noisy flag. A comparison is a paired ratio: in each iteration, NexusDI’s time divided by the other library’s time, then the median of those ratios. The bootstrap resamples blocks of consecutive iterations with a seeded generator.
The results are in benchmarks/results/timings/, one file per published run, and benchmarks/results/raw/ holds every sample of a run so the statistics can be recomputed.
Build time is the wall time of a cold build of every variant under every cell with a build step, for Meridian-8 and for a generated graph of two hundred classes. One unmeasured build per cell runs first, and at least thirty interleaved rounds follow. Deno and Node’s type stripping transpile at load, so they have no build time, and a build that failed to compile is not timed. A library’s headline build time is its fastest passing build under a toolchain its documentation names. The results are in benchmarks/results/build.json.
Wiring-Mistake Probes
Performance isn’t the only metric. We use “probes” to measure how libraries handle errors. We intentionally introduce mistakes (like circular dependencies or missing providers) and record:
- When the error is reported (Type check $\rightarrow$ Container creation $\rightarrow$ Resolution).
- How many mistakes the first error names, for the probe that makes two mistakes at once.
Each probe is type-checked under strict mode, built with tsc and run. There are five:
missing-provider: Never registersNAV_CHARTS.cycle: MakesPowerRouterandShieldGriddepend on each other.captive-scoped: Gives the singletonBridgethe scopedFlightLog, and recordsnot-applicablefor a library with no scoped lifetime.wrong-dep-type: WiresShipComputertoPowerRouterwhere it takesReactorCore.two-mistakes: Combines the first two.
The results are in benchmarks/results/probes.json.
Transparency and Reproducibility
The maintainer of NexusDI wrote this harness. To ensure transparency:
- All fixtures are open source in the
benchmarks/directory. - All results are published as raw JSON files.
- Any user can rerun the benchmarks locally using
npx nx run @nexusdi/benchmarks:bench. - The runs use GitHub-hosted runners, and each results file records the runner’s CPU and every pinned version, Node included.
The harness covers Meridian-8 and the generated build graph under every toolchain cell. It does not time Bun or Deno at run time, or a browser. It leaves out incremental and watch builds, editor type checks, graphs larger than eight providers at run time, async factories and framework integrations.
A maintainer or user who finds a fixture that departs from a library’s documentation can open an issue with the library, the fixture path, the documentation URL that shows the correct setup and the expected outcome. An accepted correction is merged and rerun, and the pages update from the new results.
Measured with
- Libraries
- NexusDI 0.4.0-rc.0InversifyJS 8.2.3 with a decorator metadata polyfilltsyringe 4.10.0 with a decorator metadata polyfillawilix 13.0.5needle-di 1.2.1
- Toolchains
- tsc 6.0.3tsgo 7.0.2esbuild 0.28.2swc 1.16.2babel 8.0.6vite 8.3.0vite8+babel-plugin 8.3.0bun 1.4.2deno 2.9.6node-strip-types 24.20.0bundlers: esbuild 0.28.2, rollup 4.63.4
- Runtime
- Node 24.20.0
- Runner
- INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GB, Linux, GitHub-hosted
- Timings run
- 2026-10-01, seed 36891702544, core commit 8824502
- Results files
matrix.jsonat a9231afprobes.jsonat a9231afsize.jsonat f699eb1build.jsonat 20d5e27timings/2026-10-01-8824502.jsonat 20d5e27
Rerunning the Benchmarks
Every deterministic results file rebuilds byte for byte. Clone the release branch and run the check, which rebuilds each one and compares it with the committed file:
git clone --branch release/0.4 https://github.com/NexusDI/core.git
cd core
npm ci
npx nx run @nexusdi/benchmarks:checkThe check packs the libraries and builds every cell, so it takes several minutes, and it exits 0.
Try it: In benchmarks/fixtures/nexusdi/plain.ts, change readonly kind = 'ReactorCore' on FusionReactor to readonly kind = 'Reactor', and run the check again. The tsc and tsgo cells fail the type check with TS2416, and the other cells print a kind that differs from golden.json, so the rebuilt matrix.json differs from the committed one. The check prints matrix.json changed. Commit the file the run wrote. and exits 1 after the matrix step, before it reaches the probes and the sizes. The failed run has already overwritten benchmarks/results/matrix.json with the broken result. Restore both files with git checkout benchmarks/fixtures/nexusdi/plain.ts benchmarks/results/matrix.json and run the check again. It exits 0.
npx nx run @nexusdi/benchmarks:bench runs the full harness, timings and builds included. Absolute times differ by machine, and the paired ratios should fall inside the published intervals.