Workers
Contributor documentation. Covers the RSC and HTML worker implementations.
In dev mode with react-server condition, workers are skipped by default — RSC renders directly on the main thread via Vite's environment runner. Set dev.useRscWorker: true to force worker usage.
RSC Worker
Renders React Server Components and handles server actions. Runs with react-server condition.
Message Handling
parentPort?.on("message", async (message) => {
switch (message.type) {
case "RSC_RENDER": await handleRscRender(message); break;
case "SERVER_ACTION": await handleServerAction(message); break;
case "CLEANUP": await handleCleanup(message); break;
case "SHUTDOWN": await handleShutdown(message); break;
}
});
RSC Rendering Flow
- Receive
RSC_RENDERwith element, moduleBaseURL, cssFiles - Call
renderToReadableStreamfromreact-server-dom-esm/server - Read chunks and send
RSC_CHUNKmessages to main thread - Send
RSC_ENDwith metrics on completion
Server Action Flow
- Receive
SERVER_ACTIONwith actionId and args - Look up action function
- Execute and send
SERVER_ACTION_RESPONSE
HTML Worker
Transforms RSC streams to HTML. Runs without react-server condition.
Flow
- Receive
ROUTE_READYwith module config and CSS - Receive
RSC_CHUNKmessages — feed into RSC-to-HTML transform - Receive
RSC_END— finalize HTML - Send
HTML_CHUNKmessages as HTML is generated - Send
HTML_COMPLETEwhen done
Environment-Specific Paths
htmlWorkerPath: `server/html-worker.${
process.env.NODE_ENV === "production" ? "production" : "development"
}.js`;
Development workers include:
- Verbose logging
- Stack traces in stream output
- MessageChannel-based loader communication
Custom Workers
Replace built-in workers with custom implementations:
vitePluginReactServer({
htmlWorkerPath: "./my-html-worker.js",
rscWorkerPath: "./my-rsc-worker.js",
});
Custom workers must implement the same message protocol. They become part of your build output.
Message Protocol
Main → Worker
| Type | Fields | Purpose |
|---|---|---|
ROUTE_READY | moduleRootPath, moduleBaseURL, cssFiles, pipeableStreamOptions | Initialize route render |
RSC_CHUNK | chunk (ArrayBuffer) | RSC content chunk |
RSC_END | — | End of RSC stream |
CLEANUP | — | Free route resources |
SHUTDOWN | — | Terminate worker (id: "*" for all) |
Worker → Main
| Type | Fields | Purpose |
|---|---|---|
READY | id, env | Worker initialized |
HTML_CHUNK | chunk (ArrayBuffer), encoding | HTML output chunk |
HTML_COMPLETE | success, html?, metrics? | Rendering done |
CHUNK_PROCESSED | success | RSC chunk acknowledged |
ERROR | error, errorInfo? | Error occurred |
Worker Termination Ordering
Build cleanup in plugin/react-static/plugin.client.ts and plugin/react-static/plugin.server.ts await worker.terminate() inside the finally block, after the cooperative SHUTDOWN protocol times out or completes. The await is load-bearing: without it, libuv-level handles for file reads/writes the worker had in flight at exit can fire AFTER doBuild restores cwd, producing post-teardown ENOENT errors against relative paths the worker resolved while cwd was the test fixture root. If you add a new build path that spawns a worker, mirror the same try { SHUTDOWN } catch {} finally { await worker.terminate() } shape.
Timeout Configuration
| Option | Default | Purpose |
|---|---|---|
rscTimeout | 5000ms | RSC render completion |
htmlTimeout | 15000ms | HTML generation completion |
rscWorkerStartupTimeout | 5000ms | RSC worker ready signal |
htmlWorkerStartupTimeout | 5000ms | HTML worker ready signal |
Timeouts are safety nets for infinite loops, not normal operation (typical renders complete in 5-30ms).