Overview
COSMOS RSC uses a two-phase rendering architecture that combines React Server Components (RSC) and traditional server-side rendering (SSR) to deliver interactive applications with optimal performance.
Rendering Architecture
Phase 1: RSC Rendering
The first phase renders React Server Components and generates an RSC payload:
// From core/server/index.js
const { renderToPipeableStream } = require('react-server-dom-webpack/server');
const payload = {
rootLayout,
tree,
serverActionResult,
formState,
};
const rscStream = renderToPipeableStream(payload, webpackMap, {
onError: (error) => {
console.error('Render error:', error);
},
});
Phase 2: SSR (HTML Generation)
The second phase consumes the RSC payload and generates HTML:
// From core/server/lib/fizz-worker.js
const { renderToPipeableStream } = require('react-dom/server');
const htmlStream = renderToPipeableStream(
createElement(SSRApp, {
initialState: { tree },
rootLayout,
}),
{
formState,
bootstrapScripts: ['/client.js'],
onShellReady: () => {
htmlStream
.pipe(injectRSCPayload(payloadConsumerRSCStream))
.pipe(writableStream);
},
}
);
Render Phases
COSMOS RSC manages three distinct render phases during request handling:
START
Initial phase when a request is received. The server initializes the app store with cookie data and metadata.
const appStore = {
metadata: {
renderPhase: 'START',
},
cookies: {
incoming: incomingCookies,
outgoing: new Map(),
},
};
SERVER_ACTION
Executed when processing server actions (POST requests). During this phase:
- Server actions are decoded and executed
- Cookies can be modified
- Form state is processed
if (req.method === 'POST') {
metadata.renderPhase = 'SERVER_ACTION';
const serverActionId = req.headers['server-action-id'];
const args = await decodeReplyFromBusboy(bb);
serverActionResult = await serverAction.apply(null, args);
}
RSC
The main rendering phase where React Server Components are rendered:
- Server Components are executed
- Data fetching occurs
- RSC payload is generated
- Cookies are read-only (cannot be modified)
metadata.renderPhase = 'RSC';
const tree = createElement(Page, { searchParams: { ...req.query } });
const rscStream = renderToPipeableStream(payload, webpackMap, options);
Content-Type Handling
RSC-Only Response
When the client requests only the RSC payload (for client-side navigation):
if (req.headers.accept === 'text/x-component') {
res.setHeader('Content-Type', 'text/x-component');
rscStream.pipe(res);
return;
}
Full HTML Response
For initial page loads, the server generates full HTML with the RSC payload embedded:
res.setHeader('Content-Type', 'text/html');
const passThroughRSCStream = new PassThrough();
rscStream.pipe(passThroughRSCStream);
// HTML stream with embedded RSC payload
htmlStream
.pipe(injectRSCPayload(payloadConsumerRSCStream))
.pipe(writableStream);
Worker-Based Architecture
COSMOS RSC uses a worker thread for SSR rendering to isolate the HTML generation process:
Main Thread (RSC Rendering)
const fizzWorker = new Worker(FIZZ_WORKER_PATH, {
execArgv: ['--conditions', 'default'],
});
const { port1, port2 } = new MessageChannel();
fizzWorker.postMessage(request, [port2]);
passThroughRSCStream.on('data', (data) => {
port1.postMessage({
type: 'data',
data,
});
});
Worker Thread (SSR)
parentPort.on('message', async (request) => {
request.port.on('message', (message) => {
if (message.type === 'data') {
htmlConsumerRSCStream.write(message.data);
} else if (message.type === 'end') {
htmlConsumerRSCStream.end();
}
});
});
Request Flow
- Request received - Express middleware handles incoming HTTP requests
- Cookie parsing - Incoming cookies are extracted from request headers
- App store initialization - Request context is established
- Server action execution (if POST) - Actions are decoded and executed
- Page component resolution - Dynamic import of the requested page
- RSC rendering - Server Components are rendered to RSC stream
- SSR rendering (if HTML requested) - RSC payload is consumed to generate HTML
- Response streaming - Content is streamed to the client
Page Resolution
Pages are dynamically imported based on the request path:
const pagePath = `../../app/pages${req.path}`;
let Page;
try {
Page = require(pagePath).default;
} catch (error) {
logger.error(`Failed to import page: ${pagePath}`, error);
res.status(500).send('Internal Server Error');
return;
}
const tree = createElement(Page, { searchParams: { ...req.query } });
Pages must export a default component to be rendered. The component receives searchParams as props containing URL query parameters.
Server Actions
Server actions can be invoked in two ways:
Programmatic Actions
const serverActionId = req.headers['server-action-id'];
const [fileUrl, functionName] = serverActionId.split('#');
const serverAction = require(fileURLToPath(fileUrl))[functionName];
const args = await decodeReplyFromBusboy(bb);
serverActionResult = await serverAction.apply(null, args);
const fakeReq = new Request('http://localhost', {
method: 'POST',
headers: { 'Content-Type': req.headers['content-type'] },
body: Readable.toWeb(req),
duplex: 'half',
});
const formData = await fakeReq.formData();
const action = await decodeAction(formData);
const result = await action();
formState = await decodeFormState(result, formData);
Error Handling
The rendering pipeline includes error handling at multiple levels:
renderToPipeableStream(payload, webpackMap, {
onError: (error) => {
console.error('Render error:', error);
res.end();
},
});
Render errors will terminate the response stream. Ensure proper error boundaries are in place in your React components.
Static Assets
Static files from the build directory are served automatically:
app.use(express.static(BUILD_DIR));
The client bundle is automatically included in the HTML response:
bootstrapScripts: ['/client.js']