Before: serialized round trips
Every dependency asks the main process independently.
N modules = up to N fetch RPCsVitest architecture note
PR 10708 lets a worker ask the main process once for already-transformed modules, then read their cached code directly from disk. The normal fetch pipeline remains the fallback.
Vite's evaluator discovers dependencies sequentially. Previously, even an already-transformed module required a main-process fetch RPC. The snapshot moves only the lookup and file transfer off that repeated path.
Every dependency asks the main process independently.
N modules = up to N fetch RPCsOne map returns cache paths for the known graph.
N warm modules = 1 snapshot RPCDirect imports are already present in second.test.ts.importedModules after entry transform. That lets a snapshot triggered by one cold dependency collect its warm siblings. The miss case appears when a warm module sits behind a cold, untransformed intermediary.
dep1.tssecond.test.ts normally. Vite import analysis creates direct graph edges to dep1.ts, dep2.ts, and dep3.ts.dep1.ts. This first dependency call triggers fetchWarmModules.dep1.ts happened to trigger the call.second.test.ts, dep2.ts, and dep3.ts; cold dep1.ts is omitted.dep1.ts uses normal RPC, while later requests for dep2.ts and dep3.ts read their snapshot paths locally.Contrast the direct-import case with two chains whose unique intermediary differs:
second.test.ts import analysis can expose only its direct second-dep.ts edge. Because that node has never been transformed, its importedModules does not yet reveal shared-dep.ts. The first snapshot therefore contains the entry only; both dependencies fall back in this context.
Conceptually, the worker sends the scope of the current run. The server returns a lookup table for only the already-warm parts of that scope. This is the relevant new information, not the exact serialized TypeScript shape.
fetchWarmModules(
"node",
[
"/project/test/second.test.ts"
]
)
These are the RPC's two positional arguments: environment name and current run entry paths. The server appends configured setup-file paths before walking the graph.
{
// The just-fetched entry is part of the walk.
"/test/second.test.ts": {
cached: true,
tmp: "/tmp/vitest/.../a1b2",
file: "/project/test/second.test.ts",
id: "/project/test/second.test.ts",
url: "/test/second.test.ts",
invalidate: false
},
// Alias with the same descriptor values
"/project/test/second.test.ts": { ... },
"/src/dep2.ts": {
cached: true,
tmp: "/tmp/vitest/.../d2",
id: "/project/src/dep2.ts",
url: "/src/dep2.ts",
...
},
"/src/dep3.ts": {
cached: true,
tmp: "/tmp/vitest/.../d3",
id: "/project/src/dep3.ts",
url: "/src/dep3.ts",
...
},
// dep1.ts is absent: its node is cold.
"/node_modules/pkg/index.js": {
externalize: "file:///.../pkg/index.js",
type: "module"
}
}
This is the happy direct-import snapshot. The triggering dep1.ts is absent, but its warm sibling nodes are already visible through second.test.ts.importedModules.
Keeping an already-evaluated module is still the cheapest path, but that reuse belongs to one worker runtime. With multiple non-isolated workers, each runtime has its own evaluated-module cache and must perform an initial fetch; snapshots can help one worker reuse server work performed by another.
isolate: falseThe same worker keeps its evaluated module cache across test files. A shared dependency such as shared-dep.ts is reused as a live module.
isolate: true + warm snapshotThe fresh worker-side module cache must instantiate shared-dep.ts again. The PR makes obtaining its transformed source cheaper.
isolate: false remains the larger optimization for repeated imports inside the same runtime. It is not snapshot-free: with multiple workers, every runtime has a first-load boundary, so one worker can use the same server-warm snapshot path after another worker has populated the graph. isolate: true creates that boundary much more often.
The key timing decision is step 3: the entry uses the old path first. Only the first dependency requests the snapshot, after entry transform and import analysis have connected the graph.
init → execute → runTestsstartVitestModuleRunnertransport.fetchModule.importer == nullrpc.fetch(entry)importer != nullfetchWarmModules(environment, files)importedModules.id or unwrapped rawId.readFileSync(warmResult.tmp)rpc.fetch(id, importer, ...)These rules make the optimization incremental and preserve the existing module-loader behavior.
The entry has no importer and follows the normal fetch path. Its transform connects the graph before the snapshot is requested.
The worker shares one snapshot promise while workerState.ctx is unchanged. If that runtime receives another run request, its new context requests another snapshot.
A missing graph node, missing disk path, or deleted temporary file uses the original rpc.fetch path.
transformResult, so the next graph walk omits them. The worker then fetches those modules live instead of trusting an old disk path.
This is the single implementation reference. File names link to the reviewed commit, while bar lengths encode additions. The warm-module mechanism is concentrated in the first two rows; gray rows belong to the separate Node compile-cache optimization.