This guide explores the internal implementation of React Server Components in COSMOS RSC, covering the complete rendering pipeline from server to client.
Architecture overview
The RSC implementation consists of three main layers:
- Server rendering - Generates RSC payload from React components
- SSR with streaming - Converts RSC payload to HTML with embedded data
- Client hydration - Reconstructs React tree and hydrates the DOM
Server-side rendering flow
Request handling
When a request arrives, the server follows this flow (core/server/index.js:42):
async function requestHandler(req, res) {
const appStore = {
metadata: { renderPhase: 'START' },
cookies: {
incoming: incomingCookies,
outgoing: new Map(),
},
};
runWithAppStore(appStore, async () => {
// Handle server actions if POST request
if (req.method === 'POST') {
metadata.renderPhase = 'SERVER_ACTION';
// ... server action execution
}
metadata.renderPhase = 'RSC';
// ... RSC rendering
});
}
Render phases
The server tracks the current rendering phase:
- START - Initial request received
- SERVER_ACTION - Executing server actions (POST requests)
- RSC - Rendering React Server Components
Component tree construction
The server constructs the React tree (core/server/index.js:106):
const pagePath = `../../app/pages${req.path}`;
const Page = require(pagePath).default;
const tree = createElement(Page, { searchParams: { ...req.query } });
let rootLayout;
if (req.headers.accept !== 'text/x-component') {
rootLayout = createElement(RootLayout, null, createElement(Slot));
}
The tree includes:
- Root layout (for initial requests)
- Page component with search params
- Server action results (if applicable)
RSC payload generation
The server renders the tree to an RSC stream (core/server/index.js:129):
const webpackMap = await getReactClientManifest();
const payload = {
rootLayout,
tree,
serverActionResult,
formState,
};
const rscStream = renderToPipeableStream(payload, webpackMap, {
onError: (error) => {
console.error('Render error:', error);
res.end();
},
});
The renderToPipeableStream function from react-server-dom-webpack/server:
- Serializes the component tree
- Replaces client component instances with references
- Streams the payload as it’s generated
Client component references
When the server encounters a client component, it:
- Looks up the component in the webpack manifest
- Replaces the component with a reference object
- Includes the module ID and chunk information
Example reference format:
{
"$$typeof": "react.client.reference",
"value": {
"id": "./app/components/counter.js",
"chunks": ["client"],
"name": "default"
}
}
SSR with HTML streaming
Two-pass rendering
For initial page loads, COSMOS RSC uses a two-pass approach:
- RSC rendering - Generate the RSC payload
- Fizz rendering - Convert payload to HTML
This happens in the Fizz worker (core/server/lib/fizz-worker.js).
Worker thread architecture
The server uses a worker thread to isolate SSR rendering:
const fizzWorker = new Worker(FIZZ_WORKER_PATH, {
execArgv: ['--conditions', 'default'],
});
Benefits:
- Isolates SSR from the main server thread
- Prevents blocking during heavy rendering
- Uses separate module resolution conditions
Streaming coordination
The server coordinates two streams using MessageChannel (core/server/index.js:154):
const passThroughRSCStream = new PassThrough();
rscStream.pipe(passThroughRSCStream);
const { port1, port2 } = new MessageChannel();
fizzWorker.postMessage({ port: port2 }, [port2]);
passThroughRSCStream.on('data', (data) => {
port1.postMessage({ type: 'data', data });
});
passThroughRSCStream.on('end', () => {
port1.postMessage({ type: 'end' });
});
The RSC stream is:
- Piped through a PassThrough stream
- Sent to the worker via MessageChannel
- Consumed by Fizz for HTML generation
Fizz worker implementation
Inside the worker (core/server/lib/fizz-worker.js:13):
parentPort.on('message', async (request) => {
const htmlConsumerRSCStream = new PassThrough();
const payloadConsumerRSCStream = new PassThrough();
// Receive RSC chunks from main thread
request.port.on('message', (message) => {
if (message.type === 'data') {
htmlConsumerRSCStream.write(message.data);
payloadConsumerRSCStream.write(message.data);
} else if (message.type === 'end') {
htmlConsumerRSCStream.end();
payloadConsumerRSCStream.end();
}
});
// Reconstruct React tree from RSC payload
const serverConsumerManifest = await getReactSSRManifest();
const { rootLayout, tree, formState } = await createFromNodeStream(
htmlConsumerRSCStream,
serverConsumerManifest
);
// Render to HTML
const htmlStream = renderToPipeableStream(
createElement(SSRApp, { initialState: { tree }, rootLayout }),
{
formState,
bootstrapScripts: ['/client.js'],
onShellReady: () => {
htmlStream
.pipe(injectRSCPayload(payloadConsumerRSCStream))
.pipe(writableStream);
},
}
);
});
The worker:
- Receives RSC payload chunks
- Reconstructs the React tree using
createFromNodeStream
- Renders to HTML using React’s Fizz renderer
- Injects RSC payload into the HTML
RSC payload injection
The injectRSCPayload transform (core/rsc-html-stream/server.js:9) embeds the RSC payload in the HTML:
function writeChunk(chunk, transform) {
transform.push(
encoder.encode(
`<script>${escapeScript(
`(self.__RSC_PAYLOAD||=[]).push(${chunk})`
)}</script>`
)
);
}
Each RSC chunk becomes a <script> tag that:
- Pushes data to the
window.__RSC_PAYLOAD array
- Executes before the client hydration code
- Provides data for client-side reconstruction
Client-side hydration
Initial hydration
The client entry point (core/client/index.js:11) hydrates the document:
async function hydrateDocument() {
const { rootLayout, tree, formState } = await createFromReadableStream(
rscStream,
{ callServer }
);
routerCache.set(getFullPath(window.location.href), tree);
const initialState = { tree, commitPendingNavigation: () => {} };
const app = (
<StrictMode>
<ErrorBoundary>
<BrowserApp rootLayout={rootLayout} initialState={initialState} />
</ErrorBoundary>
</StrictMode>
);
hydrateRoot(document, app, { formState });
}
RSC stream reconstruction
The client creates a ReadableStream from the embedded payload (core/rsc-html-stream/client.js:6):
export const rscStream = new ReadableStream({
start(controller) {
let handleChunk = (chunk) => {
if (typeof chunk === 'string') {
controller.enqueue(encoder.encode(chunk));
} else {
controller.enqueue(chunk);
}
};
window.__RSC_PAYLOAD ||= [];
window.__RSC_PAYLOAD.forEach(handleChunk);
window.__RSC_PAYLOAD.length = 0;
window.__RSC_PAYLOAD.push = (chunk) => {
handleChunk(chunk);
};
streamController = controller;
},
});
This:
- Processes existing chunks from
window.__RSC_PAYLOAD
- Overrides the array’s
push method to capture new chunks
- Creates a ReadableStream for React consumption
Client navigation
Navigation flow
For client-side navigation (core/client/lib/app-reducer.js:9):
case APP_ACTION.NAVIGATE: {
const { path, navigationType, commitPendingNavigation } = action.payload;
// Use cached tree for back/forward navigation
if (navigationType === 'traverse' && routerCache.has(path)) {
return {
...prevState,
tree: routerCache.get(path),
commitPendingNavigation,
};
}
// Fetch new RSC payload
const tree = await getRSCPayload(path);
routerCache.set(path, tree);
return {
...prevState,
tree,
commitPendingNavigation,
};
}
Fetching RSC payloads
The client requests RSC payloads with a special header (core/client/lib/get-rsc-payload.js:4):
export async function getRSCPayload(url) {
const headers = new Headers();
headers.append('accept', 'text/x-component');
const response = await fetch(url, { headers });
const { tree } = await createFromReadableStream(response.body, {
callServer,
});
return tree;
}
When the server sees accept: text/x-component, it:
- Skips the root layout
- Returns only the RSC payload (no HTML)
- Streams the component tree directly
Server actions
Client-side invocation
When a server action is called (core/client/lib/call-server.js:4):
export function callServer(id, args) {
const { promise, resolve, reject } = Promise.withResolvers();
dispatchAppAction({
type: APP_ACTION.SERVER_ACTION,
payload: { id, args },
resolve,
reject,
});
return promise;
}
Posting server actions
The action is sent to the server (core/client/lib/post-server-action.js:7):
export async function postServerAction(id, args) {
const headers = new Headers();
headers.append('server-action-id', id);
headers.append('accept', 'text/x-component');
const response = await fetch('', {
method: 'POST',
headers,
body: await encodeReply(args),
});
return createFromReadableStream(response.body, { callServer });
}
The encodeReply function serializes arguments including:
- Primitives and objects
- FormData and File objects
- Functions (as references to other server actions)
Server-side execution
The server executes the action (core/server/index.js:74):
const serverActionId = req.headers['server-action-id'];
if (serverActionId) {
const bb = busboy({ headers: req.headers });
req.pipe(bb);
const [fileUrl, functionName] = serverActionId.split('#');
const serverAction = require(fileURLToPath(fileUrl))[functionName];
const args = await decodeReplyFromBusboy(bb);
serverActionResult = await serverAction.apply(null, args);
}
After execution:
- The action result is included in the RSC payload
- The page is re-rendered with the new data
- The client receives both the tree and action result
Key implementation details
Module registration
Server code uses special Node.js module loaders to handle React and JSX files.
From core/server/index.js:1:
require('react-server-dom-webpack/node-register')();
require('@babel/register')({
ignore: [/[\/](.cosmos-rsc|node_modules)[\/]/],
presets: [['@babel/preset-react', { runtime: 'automatic' }]],
plugins: ['@babel/plugin-transform-modules-commonjs'],
});
Router cache
The client maintains a cache of fetched trees:
- Keyed by full path (pathname + search)
- Used for back/forward navigation
- Prevents re-fetching for previously visited pages
Error boundaries
Both server and client use error boundaries:
- Server errors end the response stream
- Client errors display fallback UI
- Errors during navigation preserve the previous state
Streaming benefits
- Early flush - HTML starts streaming before React finishes rendering
- Progressive hydration - Client can start processing data as it arrives
- Concurrent rendering - Server and client work in parallel
Optimization opportunities
Future improvements:
- Selective hydration for large pages
- Prefetching RSC payloads on link hover
- Caching RSC payloads in service workers
- Partial page updates without full re-renders