Skip to content

runtime: memoize vendored-package resolution in resolveSpec - #796

Open
pullfrog[bot] wants to merge 1 commit into
mainfrom
pullfrog/795-memoize-vendored-resolution
Open

runtime: memoize vendored-package resolution in resolveSpec#796
pullfrog[bot] wants to merge 1 commit into
mainfrom
pullfrog/795-memoize-vendored-resolution

Conversation

@pullfrog

@pullfrog pullfrog Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #795.

resolveSpec step 2 resolved every @oxc-project/runtime helper specifier through __require.resolve on each import site. __require has a nub-internal parent, so each call re-enters the registered resolve hook, takes the isNubInternalParent branch, and pays a createRequire plus a nested hook round trip. There are only a handful of distinct helper specifiers, but one import site per decorated file.

Transpiled decorator output emits three helper imports (decorate / decorateMetadata / decorateParam) per decorated file, so this landed on every file of a NestJS-shaped tree.

Measured

Generated fixtures, published nub 0.7.5, Node 24.18, Linux x64. Cost tracks the number of helper import sites per file, ~18 ms each:

fixture (100 files) helper imports/file before
no decorators 0 0.19s
class decorator, no metadata 1 2.20s
param decorators, no metadata 2 3.97s
class + param + method, emitDecoratorMetadata 3 5.75s

End to end, with the change:

fixture before after
200 files, plain TS 0.22s 0.21s
200 files, decorated 10.84s 0.31s
400 files, decorated 21.52s 0.37s

A CPU profile of the 100-file decorated run attributes 92.4% of wall time to resolveSpec, of which 31.9% is createRequire and 21.8% pathToFileURL — all inside the re-entrant resolve this change removes.

Correctness

The cache key is the specifier and the value is a resolved URL. A vendored package lives in nub's own distribution, so its location cannot change under a running process. The Map is module-scoped, so each realm (main thread, worker, async-tier loader worker) carries its own — same as the existing resolvingInternal flag.

Verified decorator semantics are unchanged with the patch applied: class, method and parameter decorators all fire in order, design:paramtypes metadata is emitted correctly, and the decorated class behaves the same.

No test added. This is perf-shaped — a timing assertion would be flaky on shared runners, and the observable behavior (resolution result) is already covered.

Note: the second half of #795, nub --watch replaying full startup on every edit, is a separate issue and is not addressed here.

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

resolveSpec resolved every @oxc-project/runtime helper specifier through
__require.resolve on each import site. That require has a nub-internal
parent, so it re-entered the registered resolve hook and paid a
createRequire plus a nested hook round trip per call.

Transpiled decorator output emits three helper imports per decorated
file, so the cost landed on every file of a decorator-heavy tree. Cache
the resolved URL per specifier; a vendored package lives in nub's own
distribution and cannot move under a running process.
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview Aug 27, 2026 7:28am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

I've tested nub on our nestjs project, it the startup of nub went up from 5s to 15s.

0 participants