Repository navigation
Description
Summary
In vite dev (seen with SvelteKit), the workflow:hot-update plugin starts a full builder.build() for every deleted .ts/.js file anywhere Vite watches, whether or not the file had anything to do with workflows. Builds go through createBuildQueue, which runs them one after another but never merges builds that are waiting. A branch switch or rebase that deletes many files therefore queues one full build per deleted file. Those builds then run back to back for a long time: by our measurements, deleting about 1,400 files would keep the dev server rebuilding for nearly three hours. Memory grows the whole time, and our dev server reached about 32 GB before we killed it.
Versions
workflow4.8.8,@workflow/sveltekit4.0.23,@workflow/vite4.0.23,@workflow/builders4.1.13vite8.1.1,@sveltejs/kit2.63.0- Node 24.22.1, Linux
The code below is unchanged in the latest release, @workflow/vite and @workflow/builders 5.0.6. #4385 removed the per-environment doubling; everything else still applies.
Reproduction
- In a SvelteKit app using
workflowPlugin()fromworkflow/sveltekit, startvite dev. - Create 30 plain
.tsfiles undersrc/libwith no directives, and let the server settle. - Delete them all at once.
On 4.0.23 this runs 60 builds (2 per file, once for each Vite environment), and src/routes/.well-known/workflow/v1/flow/+server.js is rewritten every 4–5 s for about 95 s. On 5.0.6 it should be 30 builds.
Causes
1. Deleted files always rebuild
dist/hot-update.js:
try {
content = await read();
}
catch {
// File might have been deleted - trigger rebuild to update generated routes
console.log('Workflow file changed, rebuilding...');
await runBuild(() => resolvedBuilder.build());
return;
}When a file is deleted, Vite's read() throws ENOENT, so every deleted JS/TS file is treated as a possible workflow file. The builder could check whether the deleted path was one of the files it found during its last discovery, and rebuild only then.
2. Builds that are waiting are never merged
createBuildQueue in @workflow/builders is just queue = queue.then(task). A build that hasn't started yet already sees every change made before it starts, so later requests could join it instead of each adding another full build. #4445 reports the same pattern in @workflow/next's eager builder, and #3333 fixes it there; the Vite plugin needs the same fix.
3. Each build makes SvelteKit regenerate all route types
writeGeneratedFile writes ${target}.${uuid}.tmp next to each generated route in src/routes/.well-known/workflow/... and then renames it. SvelteKit's watcher sees a file added and removed under src/routes and runs a full manifest sync, which in our app rewrites about 700 files in .svelte-kit/types on every workflow build. That makes each build much more expensive than the build itself. Writing the temp file outside src/routes, for example in the OS temp dir or under .svelte-kit, and renaming it into place would avoid this (rename works across directories on the same filesystem).
Memory
The JS heap stays flat after forced GC (602 MB before and after 40 builds), but the process's RSS grows by about 6–7 MB per build and is never released. It is native memory, not JS. The in-process SWC transform isn't it: 1,200 isolated transforms added about 10 MB. The esbuild child process stayed at about 70 MB. We didn't track down the rest. With hundreds of queued builds, this is what took the dev server to about 32 GB.
Related
- @workflow/next 4.0.9 dev: eager builder queues one full rebuild per watchpack batch of its start-up scan #4445: the same queue issue in
@workflow/next - fix(next): converge workflow HMR rebuilds #3333: the fix for that, Next.js only
- Backport #4370: fix(vite): rebuild workflows once per change, not per environment #4385: deduplicates the per-environment calls in the Vite plugin
- fix(vite): rebuild changed workflow dependencies #3802 (closed): a broader rework of the Vite hot-update handling, closed so it could be revisited as an independent change
Activity