Worker Communication Architecture
Overview
The plugin has two execution modes with different communication patterns:
Dev Mode (default): No Workers
By default in development, RSC rendering happens on the main thread via Vite's Environment API (server.environments['server'].runner). No workers or MessagePorts are created. This means:
- Zero port overhead per request
- HMR works natively (environment runner handles cache invalidation)
- No listener accumulation concerns
Set dev.useRscWorker: true to opt into the worker path in dev mode.
Production / Worker Mode: MessagePort Channels
When workers are used (production builds, or dev.useRscWorker: true), the communication uses MessagePort pairs:
Startup (once)
| Channel | Ports | Purpose |
|---|---|---|
parentPort | 1 | Main worker message bus |
reactLoaderChannel | 2 | Loading React components |
cssLoaderChannel | 2 | Loading CSS files |
envLoaderChannel | 2 | Loading environment variables |
hmrChannel (dev only) | 2 | Hot module replacement |
| Total | 7-9 |
Per Request
| Channel | Ports | Purpose |
|---|---|---|
dataChannel | 2 | Streaming RSC data |
controlChannel | 2 | Control messages (end, error, metrics) |
| Total | 4 | Created via createMessageChannels() |
Port Lifecycle
Per-request ports (dataChannel, controlChannel) are not explicitly closed after the request completes. The code comments state:
"We don't close ports here — let the stream consumer manage port lifecycle. This ensures
createFromNodeStream()can fully consume the stream."
Cleanup relies on garbage collection. This is intentional to avoid premature port closure while React is still consuming the stream, but means ports may accumulate under high concurrency.
setMaxListeners Helper
The plugin sets high listener limits (15-20) on ports via plugin/stream/setMaxListeners.ts to suppress Node.js MaxListenersExceededWarning. This is a workaround, not a fix — the real solution would be explicit port cleanup after stream consumption.
Loader Channels
All three loader channels (react, css, env) are created at worker startup regardless of whether they're all needed. In practice:
reactLoaderChannel: Always used (loads page/root/html components)cssLoaderChannel: Used when CSS files exist for the routeenvLoaderChannel: Used for environment variable injection
These could be lazy-initialized but the overhead is minimal since they're created once.
Potential Improvements
- Explicit port cleanup: Close
dataChannel/controlChannelports after the response stream ends. Would need careful timing to not cut off React's stream consumption. - Port pooling: Reuse data/control port pairs across requests instead of creating new ones.
- Lazy loader channels: Only create
cssLoaderChannel/envLoaderChannelwhen first needed.
None of these are urgent since the default dev mode doesn't use workers at all. Production builds use parallel rendering (batch size 8 by default), but each page gets its own worker which is terminated after use.