Overview
COSMOS RSC supports streaming SSR, which allows the server to send HTML to the browser incrementally as components finish rendering. This keeps your page interactive even while slow data is loading.
How streaming works
With streaming SSR:
Server sends the initial HTML shell immediately
Client displays the shell and shows loading states
Server streams in content as components finish rendering
Client progressively updates the page without blocking
Using Suspense
Wrap async components in Suspense boundaries to enable streaming:
app/pages/features/streaming.js
import { Suspense } from 'react' ;
// Async component that takes time to load
async function SlowData ({ delay , label }) {
await new Promise (( resolve ) => setTimeout ( resolve , delay ));
return (
< div className = 'rounded bg-white p-4 shadow' >
< h3 className = 'font-medium' > { label } </ h3 >
< p > Data loaded after { delay } ms </ p >
</ div >
);
}
// Loading fallback
function LoadingCard () {
return (
< div className = 'animate-pulse rounded bg-gray-50 p-4 shadow' >
< div className = 'mb-2 h-4 w-1/4 rounded bg-gray-200' ></ div >
< div className = 'h-4 w-3/4 rounded bg-gray-200' ></ div >
</ div >
);
}
export default function StreamingDemo () {
return (
< div >
< h1 > Streaming Demo </ h1 >
< div className = 'grid gap-4' >
< Suspense fallback = { < LoadingCard /> } >
< SlowData delay = { 1000 } label = 'Fast Component' />
</ Suspense >
< Suspense fallback = { < LoadingCard /> } >
< SlowData delay = { 3000 } label = 'Medium Component' />
</ Suspense >
< Suspense fallback = { < LoadingCard /> } >
< SlowData delay = { 5000 } label = 'Slow Component' />
</ Suspense >
</ div >
</ div >
);
}
Each Suspense boundary:
Shows the fallback immediately
Streams the real content when ready
Updates without blocking other components
Without Suspense, the page would wait for all components to finish before sending any HTML.
Streaming implementation
COSMOS RSC implements streaming using React DOM Server’s renderToPipeableStream:
core/server/lib/fizz-worker.js
const { renderToPipeableStream } = require ( 'react-dom/server' );
const { injectRSCPayload } = require ( '../../rsc-html-stream/server' );
const htmlStream = renderToPipeableStream (
createElement ( SSRApp , {
initialState: { tree },
rootLayout ,
}),
{
formState ,
bootstrapScripts: [ '/client.js' ],
onShellReady : () => {
// Start streaming as soon as shell is ready
htmlStream
. pipe ( injectRSCPayload ( payloadConsumerRSCStream ))
. pipe ( writableStream );
},
}
);
Key points:
onShellReady fires when the initial HTML is ready
Content streams in as Suspense boundaries resolve
RSC payload is injected inline as content arrives
RSC payload injection
The streaming implementation injects the RSC payload into the HTML:
core/rsc-html-stream/server.js
function injectRSCPayload ( rscStream ) {
const transform = new Transform ({
async flush ( callback ) {
await flightDataPromise ;
// Write any buffered chunks
this . push ( encoder . encode ( trailer ));
callback ();
},
});
return transform ;
}
async function writeRSCStream ( rscStream , transform ) {
for await ( const chunk of rscStream ) {
writeChunk ( JSON . stringify ( decoder . decode ( chunk )), transform );
}
}
function writeChunk ( chunk , transform ) {
transform . push (
encoder . encode (
`<script> ${ escapeScript (
`(self.__RSC_PAYLOAD||=[]).push( ${ chunk } )`
) } </script>`
)
);
}
This embeds RSC data as inline script tags throughout the HTML stream.
Client-side streaming
The client reads the streamed RSC payload:
core/rsc-html-stream/client.js
export const rscStream = new ReadableStream ({
start ( controller ) {
let handleChunk = ( chunk ) => {
if ( typeof chunk === 'string' ) {
controller . enqueue ( encoder . encode ( chunk ));
} else {
controller . enqueue ( chunk );
}
};
// Handle inline script chunks
window . __RSC_PAYLOAD ||= [];
window . __RSC_PAYLOAD . forEach ( handleChunk );
window . __RSC_PAYLOAD . push = ( chunk ) => {
handleChunk ( chunk );
};
},
});
As inline scripts execute, they push chunks to the stream which React consumes.
Nested Suspense boundaries
You can nest Suspense boundaries for fine-grained loading states:
export default function Page () {
return (
< div >
< h1 > Dashboard </ h1 >
{ /* Top-level suspense */ }
< Suspense fallback = { < DashboardSkeleton /> } >
< Dashboard >
{ /* Nested suspense for slower data */ }
< Suspense fallback = { < ChartLoading /> } >
< ExpensiveChart />
</ Suspense >
{ /* Independent suspense boundary */ }
< Suspense fallback = { < TableLoading /> } >
< DataTable />
</ Suspense >
</ Dashboard >
</ Suspense >
</ div >
);
}
This creates a hierarchy:
Page shell loads first
Dashboard shell loads next
Chart and table stream independently
Parallel data fetching
Suspense boundaries enable parallel data fetching:
// ❌ Sequential - slow
export default async function SlowPage () {
const user = await fetchUser ();
const posts = await fetchPosts ();
const comments = await fetchComments ();
return < div > { /* ... */ } </ div > ;
}
// ✅ Parallel - fast
export default function FastPage () {
return (
< div >
< Suspense fallback = { < LoadingUser /> } >
< User />
</ Suspense >
< Suspense fallback = { < LoadingPosts /> } >
< Posts />
</ Suspense >
< Suspense fallback = { < LoadingComments /> } >
< Comments />
</ Suspense >
</ div >
);
}
async function User () {
const user = await fetchUser ();
return < div > { user . name } </ div > ;
}
async function Posts () {
const posts = await fetchPosts ();
return < ul > { posts . map ( p => < li key = { p . id } > { p . title } </ li > ) } </ ul > ;
}
async function Comments () {
const comments = await fetchComments ();
return < ul > { comments . map ( c => < li key = { c . id } > { c . text } </ li > ) } </ ul > ;
}
All three components fetch data in parallel and stream independently.
Loading skeletons
Create skeleton components that match your content structure:
function ProductCardSkeleton () {
return (
< div className = 'rounded border p-4' >
< div className = 'mb-4 h-48 animate-pulse bg-gray-200' />
< div className = 'mb-2 h-6 animate-pulse bg-gray-200' />
< div className = 'h-4 w-2/3 animate-pulse bg-gray-200' />
</ div >
);
}
function ProductCard ({ productId }) {
return (
< Suspense fallback = { < ProductCardSkeleton /> } >
< ProductDetails productId = { productId } />
</ Suspense >
);
}
async function ProductDetails ({ productId }) {
const product = await fetchProduct ( productId );
return (
< div className = 'rounded border p-4' >
< img src = { product . image } alt = { product . name } />
< h3 > { product . name } </ h3 >
< p > { product . description } </ p >
</ div >
);
}
Streaming benefits
Streaming provides several advantages:
Faster Time to First Byte Server sends initial HTML immediately without waiting for all data
Progressive Enhancement Page becomes interactive before all content loads
Better Perceived Performance Users see loading states instead of blank screens
Parallel Data Fetching Multiple components fetch data simultaneously
Worker thread architecture
COSMOS RSC uses worker threads to enable concurrent HTML rendering:
const { MessageChannel , Worker } = require ( 'worker_threads' );
// Create worker on server startup
const fizzWorker = new Worker ( FIZZ_WORKER_PATH , {
execArgv: [ '--conditions' , 'default' ],
});
async function requestHandler ( req , res ) {
// Generate RSC stream
const rscStream = renderToPipeableStream ( payload , webpackMap );
// Set up message channel to worker
const { port1 , port2 } = new MessageChannel ();
fizzWorker . postMessage ({ port: port2 }, [ port2 ]);
// Pipe RSC data to worker
rscStream . on ( 'data' , ( data ) => {
port1 . postMessage ({ type: 'data' , data });
});
// Receive HTML from worker
port1 . on ( 'message' , ( message ) => {
if ( message . type === 'data' ) {
res . write ( message . data );
} else if ( message . type === 'end' ) {
res . end ();
}
});
}
This architecture:
Keeps the main thread responsive
Enables concurrent request handling
Isolates HTML rendering from RSC rendering
Next steps
Server Actions Learn about server actions for forms
Architecture Understand the full architecture