1"use strict";(self.webpackChunk_inversifyjs_inversify_docs_site=self.webpackChunk_inversifyjs_inversify_docs_site||[]).push([["4524"],{74642(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>p,frontMatter:()=>o,metadata:()=>t,toc:()=>d});var t=i(63407),s=i(89292),r=i(19736),a=i(86133);let o={slug:"a-year-of-resolution-performance",title:"A Year of Shaving Nanoseconds Off Dependency Injection",authors:["notaphplover"],tags:["performance"],image:"./cover.png"},l,c={image:i(92717).A,authorsImageUrls:[void 0]},d=[{value:"TL;DR",id:"tldr",level:2},{value:"Act 1 \u2014 InversifyJS 6: plan every single time",id:"act-1--inversifyjs-6-plan-every-single-time",level:2},{value:"Act 2 \u2014 InversifyJS 7: the <code>PlanResultCacheService</code>, and its first, blunt cache invalidation",id:"act-2--inversifyjs-7-the-planresultcacheservice-and-its-first-blunt-cache-invalidation",level:2},{value:"Act 3 \u2014 Still InversifyJS 7: context-free service nodes",id:"act-3--still-inversifyjs-7-context-free-service-nodes",level:2},{value:"Act 4 \u2014 InversifyJS 8: making the resolvers themselves faster",id:"act-4--inversifyjs-8-making-the-resolvers-themselves-faster",level:2},{value:"1. Specialized resolvers that skip work the planner already proved unnecessary",id:"1-specialized-resolvers-that-skip-work-the-planner-already-proved-unnecessary",level:3},{value:"2. Actually asking V8 to inline the call, via runtime code generation",id:"2-actually-asking-v8-to-inline-the-call-via-runtime-code-generation",level:3},{value:"The payoff: a full year of benchmarks, side by side",id:"the-payoff-a-full-year-of-benchmarks-side-by-side",level:2}];function h(e){let n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["Every ",(0,s.jsx)(n.code,{children:"container.get()"})," call has to figure out ",(0,s.jsx)(n.em,{children:"how"})," to build what was asked for. That blueprint is a ",(0,s.jsx)(n.strong,{children:"plan"})," \u2014 and for a long time, computing it was the single biggest cost of a resolution. Over roughly a year, from InversifyJS 6 through 7 and into 8, planning and resolution were rebuilt from the ground up."]}),"\n","\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"From tangled resolution graphs to lean, cached plans",src:i(35353).A+"",width:"1000",height:"420"})}),"\n",(0,s.jsx)(n.h2,{id:"tldr",children:"TL;DR"}),"\n",(0,s.jsxs)(n.p,{children:["Every time ",(0,s.jsx)(n.code,{children:"container.get()"})," runs in ",(0,s.jsx)(n.a,{href:"https://github.com/inversify/monorepo",children:"InversifyJS"}),", the library has to figure out ",(0,s.jsx)(n.em,{children:"how"})," to build the thing that was asked for: which binding to use, which constructor arguments to inject, which properties to fill in, whether an activation handler needs to run. That blueprint is called a ",(0,s.jsx)(n.strong,{children:"plan"}),", and computing it used to be the single biggest cost of a resolution."]}),"\n",(0,s.jsxs)(n.p,{children:["Over roughly a year \u2014 from InversifyJS 6 through 7 and into 8 \u2014 planning and resolution were rebuilt from the ground up: a plan cache, a way to safely reuse cached sub-plans without invalidating everything on every ",(0,s.jsx)(n.code,{children:"bind()"}),"/",(0,s.jsx)(n.code,{children:"unbind()"})," call, and finally a set of specialized, JIT-compiled resolvers that strip out checks the container can prove are unnecessary. The result: ",(0,s.jsx)(n.strong,{children:"transient, deeply-nested resolutions that used to take milliseconds now take microseconds"}),", and even simple singleton lookups got 3-4x faster."]}),"\n",(0,s.jsx)(n.p,{children:"This post walks through that year, with the actual code and PRs behind it."}),"\n",(0,s.jsx)(n.h2,{id:"act-1--inversifyjs-6-plan-every-single-time",children:"Act 1 \u2014 InversifyJS 6: plan every single time"}),"\n",(0,s.jsxs)(n.p,{children:["In InversifyJS 6, there was no persistent plan. Every call to ",(0,s.jsx)(n.code,{children:"container.get(TYPE.Something)"})," walked the binding graph from scratch:"]}),"\n",(0,s.jsx)(a.A,{id:"plan-every-time",source:`sequenceDiagram 2 participant Caller 3 participant Container 4 participant Planner 5 participant Resolver 6 7 Caller->>Container: get(ServiceA) 8 Container->>Planner: plan(ServiceA) 9 Planner->>Planner: walk bindings, constraints, ancestors 10 Planner-->>Container: PlanResult (thrown away after use) 11 Container->>Resolver: resolve(PlanResult) 12 Resolver-->>Caller: instance`}),"\n",(0,s.jsxs)(n.p,{children:["For a flat singleton, that overhead is tolerable. For a deep object graph resolved as a ",(0,s.jsx)(n.code,{children:"transient"})," binding \u2014 the classic worst case for any DI container \u2014 replanning on every single call is brutal. The ",(0,s.jsx)(n.code,{children:"container-benchmarks"})," package in the monorepo still keeps ",(0,s.jsx)(n.code,{children:"inversify6"})," around as a baseline in every benchmark run, and the raw latency numbers tell the story on their own:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Scenario"}),(0,s.jsx)(n.th,{children:"inversify6 latency (avg)"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get service, singleton scope"}),(0,s.jsx)(n.td,{children:"~1.0 \u03BCs"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get service, transient scope"}),(0,s.jsx)(n.td,{children:"~5.9 \u03BCs"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get complex service, transient scope (deep graph)"}),(0,s.jsx)(n.td,{children:"~5.0 ms"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get complex service with properties, transient scope"}),(0,s.jsx)(n.td,{children:"~5.7 ms"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["(Figures pulled from the CI benchmark bot on ",(0,s.jsx)(n.a,{href:"https://github.com/inversify/monorepo/pull/2023",children:"PR #2023"}),", which still runs ",(0,s.jsx)(n.code,{children:"inversify6"})," as a historical baseline in every release. More on how those numbers evolved below.)"]}),"\n",(0,s.jsxs)(n.h2,{id:"act-2--inversifyjs-7-the-planresultcacheservice-and-its-first-blunt-cache-invalidation",children:["Act 2 \u2014 InversifyJS 7: the ",(0,s.jsx)(n.code,{children:"PlanResultCacheService"}),", and its first, blunt cache invalidation"]}),"\n",(0,s.jsxs)(n.p,{children:["The first big idea was simple: ",(0,s.jsx)(n.strong,{children:"don't throw the plan away."})," InversifyJS 7 introduced ",(0,s.jsx)(n.a,{href:"https://github.com/inversify/monorepo/blob/master/packages/container/libraries/core/src/planning/services/PlanResultCacheService.ts",children:(0,s.jsx)(n.code,{children:"PlanResultCacheService"})}),", a service dedicated to storing ",(0,s.jsx)(n.code,{children:"PlanResult"})," objects keyed by service identifier, name and tag:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"// packages/container/libraries/core/src/planning/services/PlanResultCacheService.ts\nexport class PlanResultCacheService {\n // ...\n public get(options: GetPlanOptions): PlanResult | undefined {\n if (options.name === undefined) {\n if (options.tag === undefined) {\n return this.#getMapFromMapArray(\n this.#serviceIdToValuePlanMap,\n options,\n ).get(options.serviceIdentifier);\n }\n // ...\n }\n // ...\n }\n\n public set(options: GetPlanOptions, planResult: PlanResult): void {\n // stores the computed plan the first time it's built\n }\n\n public clearCache(): void {\n for (const map of this.#getMaps()) {\n map.clear();\n }\n\n for (const subscriber of this.#subscribers) {\n subscriber.clearCache();\n }\n }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This alone made repeated resolutions of the ",(0,s.jsx)(n.em,{children:"same"})," service dramatically cheaper \u2014 no replanning, just a map lookup."]}),"\n",(0,s.jsxs)(n.p,{children:["But there was an obvious problem: what happens when the application calls ",(0,s.jsx)(n.code,{children:"container.bind()"})," or ",(0,s.jsx)(n.code,{children:"container.unbind()"})," after some plans are already cached? A plan can depend on constraints evaluated against ",(0,s.jsx)(n.em,{children:"ancestor"})," bindings (",(0,s.jsx)(n.code,{children:"named"}),", ",(0,s.jsx)(n.code,{children:"tagged"}),", ",(0,s.jsx)(n.code,{children:"when(...)"})," conditions that inspect the parent request). There was no cheap way to know, in general, whether a newly added or removed binding invalidated an existing cached plan."]}),"\n",(0,s.jsxs)(n.p,{children:["So the first shipped strategy was blunt but correct: ",(0,s.jsx)(n.strong,{children:"any binding service mutation cleared the entire cache"})," (",(0,s.jsx)(n.code,{children:"clearCache()"}),", invoked transitively down to child container plan caches via ",(0,s.jsx)(n.code,{children:"subscribe"}),"). Safe, but it meant that any application doing incremental binding (feature flags, plugin registration, request-scoped child containers with per-request bindings) paid the full replanning cost again and again."]}),"\n",(0,s.jsx)(n.h2,{id:"act-3--still-inversifyjs-7-context-free-service-nodes",children:"Act 3 \u2014 Still InversifyJS 7: context-free service nodes"}),"\n",(0,s.jsxs)(n.p,{children:["The fix required answering a structural question first: ",(0,s.jsx)(n.strong,{children:"which plan nodes can safely outlive a binding change, and which can't?"})]}),"\n",(0,s.jsxs)(n.p,{children:["A useful analogy comes from formal language theory. In the Chomsky hierarchy, a ",(0,s.jsx)(n.em,{children:"context-free"}
12)," grammar production only depends on the symbol being produced \u2014 never on the surrounding symbols. ",(0,s.jsx)(n.a,{href:"https://github.com/inversify/monorepo/pull/823",children:'PR #823 \u2014 "Update plan to reuse cached service nodes"'})," applied the same idea to planning:"]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["A ",(0,s.jsx)(n.code,{children:"PlanServiceNode"})," is ",(0,s.jsx)(n.strong,{children:"context-free"})," if and only if it can be planned regardless of the state of its ancestors."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Concretely, the planner already tracks whenever a binding constraint calls ",(0,s.jsx)(n.code,{children:"BindingConstraints.getAncestor(...)"})," \u2014 that's the exact signal that a node's plan depends on something outside itself. If that call never happens while building a node, the node is context-free and its computed sub-plan can be cached and reused independently of ",(0,s.jsx)(n.em,{children:"where"})," in the tree it's being requested from:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"// packages/container/libraries/core/src/planning/actions/curryBuildPlanServiceNode.ts\nserviceNode.isContextFree =\n !bindingConstraintsList.last.elem.getAncestorsCalled;\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This had a structural consequence worth calling out: to make nodes safely shareable, ",(0,s.jsx)(n.strong,{children:"plan nodes could no longer hold a reference to their parent"})," \u2014 a node with a ",(0,s.jsx)(n.code,{children:"parent"})," pointer is implicitly tied to one position in one tree, which defeats the whole point of reusing it elsewhere. So ",(0,s.jsx)(n.code,{children:"parent"})," was removed from ",(0,s.jsx)(n.code,{children:"PlanServiceNode"})," and ",(0,s.jsx)(n.code,{children:"BaseBindingNode"})," entirely, replaced by explicit tracking as the plan is built (for circular-dependency and redirection error messages, among other things)."]}),"\n",(0,s.jsx)(a.A,{id:"context-free-nodes",source:`graph TD 13 A["Root: ServiceA (context-dependent, tagged)"] --> B["ServiceB (context-free)"] 14 A --> C["ServiceC (context-free)"] 15 B --> D["Logger (context-free, shared)"] 16 C --> D 17`}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsxs)(n.em,{children:[(0,s.jsx)(n.code,{children:"Logger"})," is planned once, cached without a parent reference, and reused for both ",(0,s.jsx)(n.code,{children:"ServiceB"}),"'s and ",(0,s.jsx)(n.code,{children:"ServiceC"}),"'s subtrees \u2014 even though the root request depends on ancestor tags that ",(0,s.jsx)(n.code,{children:"Logger"})," itself never inspects."]})}),"\n",(0,s.jsxs)(n.p,{children:["With this in place, cache invalidation could finally become surgical instead of total: ",(0,s.jsx)(n.a,{href:"https://github.com/inversify/monorepo/blob/master/packages/container/libraries/core/src/planning/services/PlanResultCacheService.ts",children:(0,s.jsx)(n.code,{children:"invalidateServiceBinding"})})," recomputes only the affected map entries and non-context-free nodes, instead of nuking everything. The changelog entry for ",(0,s.jsx)(n.code,{children:"@inversifyjs/[email protected]"})," (the release that shipped PR #823) records exactly this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"## 6.0.0\n### Major Changes\n- Updated `PlanServiceNode` with no `parent`\n- Updated `BaseBindingNode` with no `parent`\n### Minor Changes\n- Updated `BasePlanParams` with `setPlan`\n- Updated `PlanResultCacheService` with `invalidateService`\n- update plan to reuse `PlanServiceNode` objects\n"})}),"\n",(0,s.jsxs)(n.p,{children:['The benchmark bot commented directly on the PR, and the "get complex service in transient scope" case (the worst-case deep-graph scenario) is the one that moved the most: ',(0,s.jsx)(n.strong,{children:"~17x"})," faster than ",(0,s.jsx)(n.code,{children:"inversify6"}),", and meaningfully ahead of the ",(0,s.jsx)(n.em,{children:"previous"})," ",(0,s.jsx)(n.code,{children:"inversify7"})," baseline too \u2014 all while using less memory, since shared context-free nodes mean fewer duplicate plan objects sitting in the cache."]}),"\n",(0,s.jsx)(n.h2,{id:"act-4--inversifyjs-8-making-the-resolvers-themselves-faster",children:"Act 4 \u2014 InversifyJS 8: making the resolvers themselves faster"}),"\n",(0,s.jsxs)(n.p,{children:["Caching the ",(0,s.jsx)(n.em,{children:"plan"})," only gets halfway. Once a plan exists, the container still needs to ",(0,s.jsx)(n.em,{children:"execute"})," it \u2014 walk constructor arguments, inject properties, and, potentially, run activation handlers. InversifyJS 8 attacked that execution path directly, with two complementary techniques."]}),"\n",(0,s.jsx)(n.h3,{id:"1-specialized-resolvers-that-skip-work-the-planner-already-proved-unnecessary",children:"1. Specialized resolvers that skip work the planner already proved unnecessary"}),"\n",(0,s.jsxs)(n.p,{children:["The current file behind this optimization is a good example: ",(0,s.jsx)(n.a,{href:"https://github.com/inversify/monorepo/blob/master/packages/container/libraries/core/src/planning/calculations/buildNoActivationsResolvedValueBindingNodeResolverJit.ts",children:(0,s.jsx)(n.code,{children:"buildNoActivationsResolvedValueBindingNodeResolverJit.ts"})}),". At plan time, the container already knows whether a service identifier has any activation handlers registered:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"// packages/container/libraries/core/src/planning/models/ResolvedValueBindingNodeImplementation.ts\nconst areServiceActivations: boolean =\n params.operations.getActivations(binding.serviceIdentifier) !== undefined;\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Instead of building one generic resolver that always checks ",(0,s.jsx)(n.code,{children:"if (
17activations) { ... }"})," at ",(0,s.jsx)(n.em,{children:"resolve"})," time \u2014 on every single call \u2014 the planner now builds a ",(0,s.jsx)(n.strong,{children:"specialized closure once"}),", at plan time, that has already made that decision:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"// packages/container/libraries/core/src/planning/calculations/buildNoActivationsResolvedValueBindingNodeResolverJit.ts\nexport function buildNoActivationsResolvedValueBindingNodeResolverJit<TActivated>(\n node: ResolvedValueBindingNode<ResolvedValueBinding<TActivated>>,\n areServiceActivations: boolean,\n): (params: ResolutionParams) => Resolved<TActivated> {\n const resolveActivations = areServiceActivations\n ? resolveServiceActivations(serviceIdentifier)\n : undefined;\n\n // arity-specialized resolvers (0..4 args have dedicated fast paths,\n // 5+ falls back to a generic loop) to minimize call dispatch overhead\n // on the hottest resolution path\n switch (node.binding.metadata.arguments.length) {\n case ZERO_PARAMS:\n resolveNode = buildZeroResolvedValueArgumentsResolverJit(node, resolveActivations);\n break;\n // ...\n }\n\n return resolveScopedWithNoActivations(node.binding, resolveNode);\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If there are no activations for that service, ",(0,s.jsx)(n.code,{children:"resolveActivations"})," is ",(0,s.jsx)(n.code,{children:"undefined"})," for the ",(0,s.jsx)(n.em,{children:"lifetime of the plan"}),' \u2014 the generated function body never contains an activation branch at all, rather than evaluating a branch condition millions of times. The same "no activations" specialization pattern is mirrored for instance bindings and dynamic-value bindings, and it composes with per-arity resolvers (0, 1, 2, 3, 4, or N constructor arguments) so the hottest paths \u2014 zero- and one-argument constructors, which dominate most real dependency graphs \u2014 get the leanest possible function.']}),"\n",(0,s.jsx)(n.h3,{id:"2-actually-asking-v8-to-inline-the-call-via-runtime-code-generation",children:"2. Actually asking V8 to inline the call, via runtime code generation"}),"\n",(0,s.jsxs)(n.p,{children:["The arity-specialized resolvers don't just avoid branches \u2014 several of them are generated with ",(0,s.jsx)(n.code,{children:"new Function(...)"})," at plan time, producing small, monomorphic functions that V8's JIT can inline aggressively:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"// packages/container/libraries/core/src/planning/calculations/buildZeroResolvedValueArgumentsResolverJit.ts\nconst buildResolveNode = new Function(\n `node$${id}`,\n `return function resolveNode$${id}(params$${id}) {\n return node$${id}.binding.factory();\n };`,\n) as (node: ResolvedValueBindingNode<...>) => (params: ResolutionParams) => Resolved<TActivated>;\n\nreturn buildResolveNode(node);\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Every generated resolver gets a unique suffix (",(0,s.jsx)(n.code,{children:"getGeneratedResolverId()"}),") so V8 doesn't have to de-optimize a polymorphic call site shared across unrelated bindings \u2014 each binding effectively gets its own dedicated, monomorphic function shape. This is exactly the kind of low-level trick expected from a JS engine performance deep-dive, applied to a dependency injection container."]}),"\n",(0,s.jsxs)(n.p,{children:["Because generating functions via ",(0,s.jsx)(n.code,{children:"new Function"})," needs runtime code evaluation (a problem under strict CSP), InversifyJS 8.2.0 paired this with a ",(0,s.jsx)(n.code,{children:"jitless"})," container option and a non-JIT fallback path, so security-conscious deployments can opt out of the codegen trick without losing the tree of other optimizations (arity specialization, activation skipping, plan caching). See ",(0,s.jsx)(n.a,{href:"/blog/announcing-inversify-8-2/",children:"Announcing InversifyJS 8.2"})," for more on that option."]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"container/core"})," changelog entries chart the same story release by release:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"## 8.1.1 \u2013 cached resolver on the binding node + simplified core logic\n (~20% improvement on transient services with deep dependency graphs)\n## 8.1.3 \u2013 forced inlined V8 constructor calls for transient scope\n## 8.2.0 \u2013 better inlined constructor calls; instance resolution flow relies\n on inlined functions also for property-injected services;
17\n jitless option added\n## 8.2.1 \u2013 resolved-value binding resolution respects `jitless`,\n avoiding `Function` constructor usage in CSP-restricted environments\n"})}),"\n",(0,s.jsx)(n.h2,{id:"the-payoff-a-full-year-of-benchmarks-side-by-side",children:"The payoff: a full year of benchmarks, side by side"}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"@inversifyjs/container-benchmarks"})," package runs the same suite \u2014 ",(0,s.jsx)(n.code,{children:"get service"}),", ",(0,s.jsx)(n.code,{children:"get complex service"}),", ",(0,s.jsx)(n.code,{children:"get complex service with properties"}),", all in singleton and transient scopes \u2014 against every historical major version (",(0,s.jsx)(n.code,{children:"inversify6"}),", ",(0,s.jsx)(n.code,{children:"inversify7"}),", ",(0,s.jsx)(n.code,{children:"inversify8"}),") plus the in-development ",(0,s.jsx)(n.code,{children:"inversifyCurrent"}),", and against competing containers (NestJS DI, tsyringe, awilix) for context. The most recent run, from the automated release PR ",(0,s.jsx)(n.a,{href:"https://github.com/inversify/monorepo/pull/2023",children:"#2023"})," (CJS build), lines them all up:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Scenario"}),(0,s.jsx)(n.th,{children:"inversify6"}),(0,s.jsx)(n.th,{children:"inversify7"}),(0,s.jsx)(n.th,{children:"inversify8"}),(0,s.jsx)(n.th,{children:"current"}),(0,s.jsx)(n.th,{children:"current vs 6"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get service, singleton"}),(0,s.jsx)(n.td,{children:"1,072 ns"}),(0,s.jsx)(n.td,{children:"315 ns"}),(0,s.jsx)(n.td,{children:"251 ns"}),(0,s.jsx)(n.td,{children:"250 ns"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"4.2x"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get service, transient"}),(0,s.jsx)(n.td,{children:"5,853 ns"}),(0,s.jsx)(n.td,{children:"595 ns"}),(0,s.jsx)(n.td,{children:"300 ns"}),(0,s.jsx)(n.td,{children:"270 ns"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"22.0x"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get complex service, transient (deep graph)"}),(0,s.jsx)(n.td,{children:"5,016,989 ns"}),(0,s.jsx)(n.td,{children:"251,792 ns"}),(0,s.jsx)(n.td,{children:"65,969 ns"}),(0,s.jsx)(n.td,{children:"39,911 ns"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"125.9x"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Get complex service with properties, transient"}),(0,s.jsx)(n.td,{children:"5,720,696 ns"}),(0,s.jsx)(n.td,{children:"337,682 ns"}),(0,s.jsx)(n.td,{children:"78,920 ns"}),(0,s.jsx)(n.td,{children:"50,377 ns"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"114.9x"})})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["Reading this left to right ",(0,s.jsx)(n.em,{children:"is"})," the optimization timeline:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"6 \u2192 7"}),": the plan cache alone buys ~10-20x on repeat resolutions."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"7 \u2192 8"}),": context-free reuse plus specialized/JIT resolvers buys another ",(0,s.jsx)(n.strong,{children:"4-8x"})," on top of that."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"8 \u2192 current"}),": incremental refinements (arity specialization for more shapes, more inlining, activation-skip everywhere it's provably safe) keep shaving another 1.3-1.8x, with diminishing but real returns."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Multiply it end to end and the worst-case scenario \u2014 a deep, transient object graph, the exact shape that punishes a naive DI container \u2014 went from ",(0,s.jsx)(n.strong,{children:"~5 milliseconds to ~40 microseconds"}),". That's not a rounding error; it's the difference between a container that has to be architected around and one that can be ignored."]})]})}function p(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},92717(e,n,i){i.d(n,{A:()=>t});let t=i.p+"assets/images/cover-2535c7d941c5638750a233035885bb2d.png"},35353(e,n,i){i.d(n,{A:()=>t});let t=i.p+"assets/images/cover-2535c7d941c5638750a233035885bb2d.png"},86133(e,n,i){i.d(n,{A:()=>a});var t=i(89292),s=i(8001),r=i(83888);s.A.initialize({});let a=({source:e,id:n})=>{let i=(0,r.useRef)(null);return(0,r.useEffect)(()=>{void 0!==e&&(async()=>
17{if(null!==i.current){i.current.innerHTML=e;let{svg:t,bindFunctions:r}=await s.A.render(`mermaid-diagram-${n}`,e);i.current.innerHTML=t,r?.(i.current)}})()},[n,e]),(0,t.jsx)("div",{className:"mermaidContainer_eEWq",id:n,ref:i})}},63407(e){e.exports=JSON.parse('{"permalink":"/blog/a-year-of-resolution-performance","source":"@site/blog/2026-07-27-a-year-of-resolution-performance/index.mdx","title":"A Year of Shaving Nanoseconds Off Dependency Injection","description":"Every container.get() call has to figure out how to build what was asked for. That blueprint is a plan \u2014 and for a long time, computing it was the single biggest cost of a resolution. Over roughly a year, from InversifyJS 6 through 7 and into 8, planning and resolution were rebuilt from the ground up.","date":"2026-07-27T00:00:00.000Z","tags":[{"inline":false,"label":"Performance","permalink":"/blog/tags/performance","description":"InversifyJS performance related posts"}],"readingTime":9.96,"hasTruncateMarker":true,"authors":[{"name":"Roberto Pintos L\xf3pez","title":"InversifyJS maintainer","url":"https://github.com/notaphplover","page":{"permalink":"/blog/authors/notaphplover"},"socials":{"github":"https://github.com/notaphplover"},"imageURL":"https://github.com/notaphplover.png","key":"notaphplover"}],"frontMatter":{"slug":"a-year-of-resolution-performance","title":"A Year of Shaving Nanoseconds Off Dependency Injection","authors":["notaphplover"],"tags":["performance"],"image":"./cover.png"},"unlisted":false,"nextItem":{"title":"Announcing InversifyJS 8.2","permalink":"/blog/announcing-inversify-8-2"}}')}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.