The @vitejs/plugin-rsc implements React Server Components using Vite's multi-environment architecture. Each environment (rsc, ssr, client) has its own module graph, requiring a multi-pass build strategy.
rsc (scan) → ssr (scan) → rsc (real) → client → ssr (real)
| Step | Phase | Write to Disk | Purpose |
|---|---|---|---|
| 1 | RSC scan | No | Discover "use client" boundaries → clientReferenceMetaMap |
| 2 | SSR scan | No | Discover "use server" boundaries → serverReferenceMetaMap |
| 3 | RSC real | Yes | Build server components, populate renderedExports, serverChunk |
| 4 | Client | Yes | Build client bundle using reference metadata, generate buildAssetsManifest |
| 5 | SSR real | Yes | Final SSR build with complete manifests |
rsc (scan) → client (scan) → rsc (real) → client (real)
- RSC scan first: Must discover
"use client"boundaries before client build knows what to bundle - SSR scan second: Must discover
"use server"boundaries for proxy generation in both client and SSR - RSC real third: Generates proxy modules, determines which exports are actually used (
renderedExports) - Client fourth: Needs RSC's
renderedExportsto tree-shake unused client components - SSR last: Needs complete client manifest for SSR hydration
The SSR scan depends on RSC scan output. This prevents parallelization:
- SSR entry imports
@vitejs/plugin-rsc/ssr ssr.tsximportsvirtual:vite-rsc/client-references- This virtual module reads
clientReferenceMetaMap(populated during RSC scan) - Client components may import
"use server"files - SSR scan processes those imports, populating
serverReferenceMetaMap
┌─────────────────────────────────────────────────────────────┐
│ RSC Scan Build │
│ Writes: clientReferenceMetaMap (importId, exportNames) │
│ Writes: serverReferenceMetaMap (for "use server" in RSC) │
└──────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ SSR Scan Build │
│ Writes: serverReferenceMetaMap (for "use server" in SSR) │
└──────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ RSC Real Build │
│ Reads: clientReferenceMetaMap │
│ Mutates: renderedExports, serverChunk on each meta │
│ Outputs: rscBundle │
└──────────────────────────┬──────────────────────────────────┘
▼
manager.stabilize()
(sorts clientReferenceMetaMap)
▼
┌─────────────────────────────────────────────────────────────┐
│ Client Build │
│ Reads: clientReferenceMetaMap (with renderedExports) │
│ Uses: clientReferenceGroups for chunking │
│ Outputs: buildAssetsManifest, copies RSC assets │
└──────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ SSR Real Build │
│ Reads: serverReferenceMetaMap │
│ Final output with assets manifest │
└─────────────────────────────────────────────────────────────┘
Central state manager shared across all build phases:
class RscPluginManager {
server: ViteDevServer
config: ResolvedConfig
rscBundle: Rollup.OutputBundle
buildAssetsManifest: AssetsManifest | undefined
isScanBuild: boolean = false
// Reference tracking
clientReferenceMetaMap: Record<string, ClientReferenceMeta> = {}
clientReferenceGroups: Record<string, ClientReferenceMeta[]> = {}
serverReferenceMetaMap: Record<string, ServerReferenceMeta> = {}
serverResourcesMetaMap: Record<string, { key: string }> = {}
}When RSC transform encounters "use client":
- Parse exports from the module
- Generate a unique
referenceKey(hash of module ID) - Store in
clientReferenceMetaMap:importId: Module ID for importingreferenceKey: Unique identifierexportNames: List of exportsrenderedExports: Exports actually used (populated during real build)serverChunk: Which RSC chunk imports this (for grouping)
When transform encounters "use server":
- Parse exported functions
- Generate reference IDs
- Store in
serverReferenceMetaMap - Generate proxy module that calls server via RPC
Key virtual modules used in the build:
| Virtual Module | Purpose |
|---|---|
virtual:vite-rsc/client-references |
Entry point importing all client components |
virtual:vite-rsc/client-references/group/{name} |
Grouped client components for code splitting |
virtual:vite-rsc/assets-manifest |
Client asset manifest for SSR |
virtual:vite-rsc/rpc-client |
Dev-mode RPC client for cross-environment calls |
import.meta.viteRsc.loadModule(environment, entryName) enables loading modules from other environments:
Dev mode:
globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__(environmentName, resolvedId)Build mode:
- Emits marker during transform
renderChunkresolves to relative import path between output directories
| Component | Location |
|---|---|
| Manager definition | src/plugin.ts:112-148 |
| Build orchestration | src/plugin.ts:343-429 |
| clientReferenceMetaMap writes | src/plugin.ts:1386 |
| serverReferenceMetaMap writes | src/plugin.ts:1817, 1862 |
| Scan strip plugin | src/plugins/scan.ts |
| Cross-env module loading | src/plugin.ts:824-916 |
Virtual modules with \0 prefix need special handling:
- Vite convention:
\0prefix marks virtual modules - When used as import specifiers,
\0must be stripped - CSS requests get
?directquery added by Vite - The
resolved-id-proxyplugin handles query stripping
See src/plugins/resolved-id-proxy.ts for implementation.