Skip to main content
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:
  1. Server rendering - Generates RSC payload from React components
  2. SSR with streaming - Converts RSC payload to HTML with embedded data
  3. 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:
  1. Looks up the component in the webpack manifest
  2. Replaces the component with a reference object
  3. 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:
  1. RSC rendering - Generate the RSC payload
  2. 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:
  1. Receives RSC payload chunks
  2. Reconstructs the React tree using createFromNodeStream
  3. Renders to HTML using React’s Fizz renderer
  4. 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

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:
  1. The action result is included in the RSC payload
  2. The page is re-rendered with the new data
  3. 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

Performance considerations

Streaming benefits

  1. Early flush - HTML starts streaming before React finishes rendering
  2. Progressive hydration - Client can start processing data as it arrives
  3. 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

Build docs developers (and LLMs) love