Repository navigation
Conversation
With VPX_BUNDLE_CACHE=1, the first run of a script records how each import resolved and in which format each module loaded. At exit, a detached builder bundles the script's ES modules with Rolldown into node_modules/.vite-plus/vpx-bundle/. Later runs check every input and load the bundle, with the native loader loaded only for a module that the bundle does not contain. Each bundled module keeps its own import.meta values, a computed import() or import.meta.resolve() resolves from the module's own URL, and a module that code outside the bundle imports stays out, so that no module has two instances. A load of a bundled module from its own file is reported once and leaves the module out of the next build. The preload requires the cache code only when VPX_BUNDLE_CACHE=1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chunk runs all its imports before its own code, so an external module that a chunk imported directly ran before the bundled code that came before it. Each external that startup code imports now goes through a bundled proxy in a chunk of its own, between chunks of the bundled modules around it, and the entry imports them in their run order. A startup dependency with a top-level await keeps the script off the bundle, where its chunk could wait for itself. Also from review: - A failed build records every file it read, so a fix brings the bundle back. - The recorded run takes the build lock at exit, for the short build only. - A build keeps the three newest older outputs; a run checks its chunks exist. - A literal import() that does not resolve at build time resolves at run time. - Every bare specifier records its node_modules lookup. - The tsconfig variables and the TypeScript dependency rule have one source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🚀 Deploying Preview to Cloudflare 🚀Preview URL: https://feat-vpx-bundle-cache-viteplus-dev.voidzero-docs.workers.dev (commit c406bb4)This URL reflects your latest Preview deploymentPreview Deployments by commit
|
Contributor
|
✅ Staging deployment successful! Preview: https://viteplus-staging.void.app/ |
Contributor
CLI artifact sizes (
|
| Artifact | Format | Base | PR | Change |
|---|---|---|---|---|
packages/cli/dist |
Directory total | 2.34 MiB | 2.39 MiB | +57.60 KiB (+2.41%) |
packages/core/dist |
Directory total | 4.09 MiB | 4.09 MiB | 0 B (0.00%) |
| Combined package dist | Directory total | 6.42 MiB | 6.48 MiB | +57.60 KiB (+0.88%) |
vp (Linux x64) |
Binary | 8.34 MiB | 8.34 MiB | 0 B (0.00%) |
vp (Linux x64) |
gzip -9 | 3.81 MiB | 3.81 MiB | 0 B (0.00%) |
| NAPI (Linux x64) | Binary | 36.77 MiB | 36.77 MiB | 0 B (0.00%) |
| NAPI (Linux x64) | gzip -9 | 14.69 MiB | 14.69 MiB | 0 B (0.00%) |
vp (macOS ARM64) |
Binary | 5.46 MiB | 5.46 MiB | 0 B (0.00%) |
vp (macOS ARM64) |
gzip -9 | 3.25 MiB | 3.25 MiB | 0 B (0.00%) |
| NAPI (macOS ARM64) | Binary | 43.43 MiB | 43.43 MiB | 0 B (0.00%) |
| NAPI (macOS ARM64) | gzip -9 | 18.86 MiB | 18.86 MiB | 0 B (0.00%) |
vp (Windows x64) |
Binary | 6.18 MiB | 6.18 MiB | 0 B (0.00%) |
vp (Windows x64) |
gzip -9 | 2.86 MiB | 2.86 MiB | 0 B (0.00%) |
| NAPI (Windows x64) | Binary | 32.81 MiB | 32.81 MiB | 0 B (0.00%) |
| NAPI (Windows x64) | gzip -9 | 13.45 MiB | 13.45 MiB | -1 B (-0.00%) |
| Trampoline (Windows x64) | Binary | 13.50 KiB | 13.50 KiB | 0 B (0.00%) |
| Trampoline (Windows x64) | gzip -9 | 7.09 KiB | 7.09 KiB | 0 B (0.00%) |
| Installer (Windows x64) | Binary | 4.68 MiB | 4.68 MiB | 0 B (0.00%) |
| Installer (Windows x64) | gzip -9 | 2.24 MiB | 2.24 MiB | +2 B (+0.00%) |
Rolldown treats an ES module that uses `require`, `module`, or `exports`
without a declaration as CommonJS and gives it a working `require()`.
Node.js runs the same module without these globals, so code such as
node-fetch-native's `try { require(...) } catch` took the other path in
the bundle. The analysis now marks such a module as unsafe, and it loads
from its own file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a section on the cache files, the key, the hit/off/miss decision, how a bundle is made, what makes it expire, and when it is reused. Add results from more projects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With
VPX_BUNDLE_CACHE=1,vpx <script>bundles the script's ES modules with Rolldown after the first run. Later runs load the bundle while no input changed. The cache is opt-in and experimental. Without the variable, scripts run as before.This replaces the per-module transform cache of #3343. That approach could not reach 2x, because the transform takes only about 28 µs per file. Most of the startup time is per-module work: V8 compile, module resolution, Node.js' module loader, and file system calls. Plain
nodewith its own type stripping and compile cache is 1.34x faster than the currentvpxon the same workload, which is the limit for any loader that keeps one file per module.rfcs/vpx-bundle-cache.mdhas the profile and the design.Results
Knip 6.44.2 from source, scanning Marked 18.1.3 (the workload of #3343), complete run through the global
vpx, on an Apple M4 Pro. Every mode gives the same report.The first run, which records the trace, takes 349.8 ms, compared with 348.6 ms with the cache off. The bundle builds in a separate process after the run exits.
The
bench/vpx-script.tsfixtures on Node.js 24.22.2, through the loader:VPX_BUNDLE_CACHE=1hello(one file)esm-app(300 modules)cjs-app(300 modules)large-esm(1.8 MB)large-cjs(CommonJS, not bundled)Commands from other projects on Node.js 24.22.2, through the loader. Every command gave the same output with and without the cache.
VPX_BUNDLE_CACHE=1@aws-sdk/client-s3,rxjs,date-fns, andzodsrc/cli.tssrc/cli.ts --helpsrc/run.ts --helppackages/cli/src/bin.ts helpscripts/check-icon-dimensions.ts(one file)i18n/check.ts(one file, reads many JSON files)bin/prettier.cjs --check(CommonJS)The gain depends on how many modules the script loads at startup. Plain Node.js takes 536.2 ms for the library script, so its time comes from loading about 650 modules one at a time. The Vite+ CLI's
helploads only about 40 of its 296 bundled modules. A one-file script saves only the native loader's startup, about 4–7 ms.How it works
First run. The script loads with the normal hooks. The run records how each import resolved and in which format each module loaded. At exit, a detached
dist/script-bundle-build.jsloads each ES module with oxc-node's own transform and bundles it with Rolldown intonode_modules/.vite-plus/vpx-bundle/.A module stays out of the bundle and loads from its own file if it is not an ES module, if it uses
import.metaother thanurl,filename,dirname,main, andresolve(), if it uses a directeval, or if it usesrequire,module, orexportswithout a declaration. Rolldown would give such a module working CommonJS globals, which Node.js does not (node-fetch-native in taze takes itscatchpath).Later runs. The preload checks every input (the inode, size, modification time, and change time of each file), then runs the bundle with Node.js' compile cache. Chunks, built-ins, and JavaScript dependencies go straight to Node.js. The native binding loads only for a module outside the bundle. On Knip, it does not load.
The inputs are the bundled modules, each import target, the directories of relative targets outside
node_modules, the nearestpackage.jsonfiles, thenode_moduleslookups of bare specifiers, the tsconfig files, and the loader's own files. The key (Node.js version, platform, entry, working directory,execArgv,NODE_OPTIONS, tsconfig variables) selects the cache directory. The script's arguments are not in the key. One changed input makes the whole bundle expire: the next run records again at the speed without the cache. On Knip, the three runs after an edit to one file take 291.3 ms, 218.5 ms (new chunk paths, so V8 compiles them again), and 139.1 ms.Same behavior as before.
import.meta.url,filename, anddirname, andprocess.argv[1]stays the script.import()orimport.meta.resolve()resolves from the original module./*#__PURE__*/call, andkeepNameskeeps function names.No second instance of a module. A module that code outside the bundle imports or requires stays out of the bundle, and so do its imports. If a run still loads a bundled module from its own file (for example, a computed
import()that the first run did not reach), the run prints a warning, and the next build leaves the module out.Same order as before. A chunk runs all its imports before its own code. Thus each module outside the bundle that startup code imports goes through a bundled proxy (
export * from "<url>") in a chunk of its own, and the bundled modules between two such modules form a chunk. The entry has its own chunk and imports these chunks in their run order. A CommonJS dependency that readsprocess.envat load still sees what a bundled module before it set. A module that onlyimport()reaches goes into a chunk that loads with thatimport(). If a startup dependency has a top-levelawait, the script runs without a bundle, because its chunk could wait for itself.The cache is off in watch mode, under coverage, with another
--require,--import, or--loader, with--test,--eval, or--preserve-symlinks, and on Node.js releases without in-thread module hooks (before 22.22.3 and 24.11.1).VPX_BUNDLE_CACHE_DEBUG=1prints why each run uses the bundle or not, and builds before exit.The preload requires
dist/script-bundle-hooks.cjsonly whenVPX_BUNDLE_CACHE=1. The other change on the default path: thepiratesmatcher decides with a JavaScript extension check instead of a call to the binding, which gives the same answer undersetTransformDeps('typescript'). Twovpxsnapshots change only in the preload line number of a stack trace.Known limitations
import()reaches, the order is kept less strictly than at startup.🤖 Generated with Claude Code