Documentation Index Fetch the complete documentation index at: https://mintlify.com/facebook/react/llms.txt
Use this file to discover all available pages before exploring further.
renderToReadableStream
renderToReadableStream renders a React tree to a ReadableStream of HTML using the Web Streams API. This is the recommended approach for edge runtimes, Cloudflare Workers, Deno, and other modern JavaScript environments.
const stream = await renderToReadableStream ( element , options ? );
This API uses Web Streams API (ReadableStream) instead of Node.js streams. For Node.js, use renderToPipeableStream instead.
Reference
renderToReadableStream(element, options?)
Renders a React element to a ReadableStream with full Suspense support and progressive streaming.
import { renderToReadableStream } from 'react-dom/server' ;
const stream = await renderToReadableStream ( < App /> );
Parameters
element : ReactNode - The React element to render
options : Object (optional)
identifierPrefix : string - Prefix for IDs generated by useId
namespaceURI : string - Namespace URI for the document (e.g., SVG)
nonce : string | { script?: string, style?: string } - Nonce for Content Security Policy
bootstrapScriptContent : string - Inline script to run before other scripts
bootstrapScripts : Array<string | { src: string, integrity?: string, crossOrigin?: string }> - External scripts to load
bootstrapModules : Array<string | { src: string, integrity?: string, crossOrigin?: string }> - External modules to load
progressiveChunkSize : number - Size of chunks for progressive streaming
signal : AbortSignal - Signal to abort the render
onError : (error: mixed, errorInfo: ErrorInfo) => ?string - Error handler callback
importMap : ImportMap - Import map for module scripts
formState : ReactFormState | null - Form state for progressive enhancement
onHeaders : (headers: Headers) => void - Callback when headers are ready
maxHeadersLength : number - Maximum length for early hints headers
Returns
Returns a Promise that resolves to a ReactDOMServerReadableStream:
type ReactDOMServerReadableStream = ReadableStream & {
allReady : Promise < void >; // Resolves when all content is ready
};
The Promise resolves when the shell (initial UI) is ready
The stream continues emitting chunks as Suspense boundaries resolve
stream.allReady resolves when all content, including Suspense boundaries, is complete
Caveats
Async API : Returns a Promise that must be awaited
Edge optimized : Designed for edge runtimes using Web Streams
No pipe method : Use the stream directly with Response or other Web APIs
One-time use : The stream can only be read once
Usage
Basic streaming with edge runtime
Vercel Edge
Cloudflare Workers
Deno
import { renderToReadableStream } from 'react-dom/server' ;
import App from './App' ;
export const config = {
runtime: 'edge' ,
};
export default async function handler ( request ) {
const stream = await renderToReadableStream ( < App /> , {
bootstrapScripts: [ '/client.js' ],
});
return new Response ( stream , {
headers: { 'Content-Type' : 'text/html' },
});
}
import { renderToReadableStream } from 'react-dom/server' ;
import App from './App' ;
export default {
async fetch ( request , env , ctx ) {
const stream = await renderToReadableStream ( < App /> , {
bootstrapScripts: [ '/client.js' ],
});
return new Response ( stream , {
headers: {
'Content-Type' : 'text/html; charset=utf-8' ,
},
});
} ,
} ;
import { renderToReadableStream } from 'https://esm.sh/react-dom/server' ;
import App from './App.tsx' ;
Deno . serve ( async ( request ) => {
const stream = await renderToReadableStream ( < App /> , {
bootstrapScripts: [ '/client.js' ],
});
return new Response ( stream , {
headers: { 'Content-Type' : 'text/html' },
});
});
Streaming with Suspense
import { Suspense } from 'react' ;
import { renderToReadableStream } from 'react-dom/server' ;
function App () {
return (
< html >
< head >
< title > My App </ title >
</ head >
< body >
< nav >
< a href = "/" > Home </ a >
</ nav >
< main >
{ /* Shell content - sent immediately */ }
< h1 > Welcome </ h1 >
{ /* Streamed when ready */ }
< Suspense fallback = { < div > Loading posts... </ div > } >
< BlogPosts />
</ Suspense >
< Suspense fallback = { < div > Loading comments... </ div > } >
< Comments />
</ Suspense >
</ main >
</ body >
</ html >
);
}
// BlogPosts is an async server component
async function BlogPosts () {
const posts = await fetchPosts ();
return (
< ul >
{ posts . map ( post => (
< li key = { post . id } > { post . title } </ li >
)) }
</ ul >
);
}
export default async function handler () {
const stream = await renderToReadableStream ( < App /> );
return new Response ( stream , {
headers: { 'Content-Type' : 'text/html' },
});
}
Error handling
import { renderToReadableStream } from 'react-dom/server' ;
export default async function handler ( request ) {
try {
const stream = await renderToReadableStream ( < App /> , {
bootstrapScripts: [ '/client.js' ],
onError ( error , errorInfo ) {
// Log errors to your error tracking service
console . error ( 'Rendering error:' , error );
console . error ( 'Component stack:' , errorInfo . componentStack );
// Return an error digest to send to the client
// This will be available in Error Boundaries
return errorInfo . digest || 'UNKNOWN_ERROR' ;
},
});
return new Response ( stream , {
status: 200 ,
headers: { 'Content-Type' : 'text/html' },
});
} catch ( error ) {
// Shell rendering failed - return error page
console . error ( 'Fatal rendering error:' , error );
return new Response (
'<html><body><h1>Something went wrong</h1></body></html>' ,
{
status: 500 ,
headers: { 'Content-Type' : 'text/html' },
}
);
}
}
Aborting renders
Use AbortSignal to cancel rendering when the client disconnects:
import { renderToReadableStream } from 'react-dom/server' ;
export default async function handler ( request ) {
// Create an abort controller
const controller = new AbortController ();
// Abort if client disconnects
request . signal . addEventListener ( 'abort' , () => {
controller . abort ();
});
// Set a timeout
const timeout = setTimeout (() => {
controller . abort ();
}, 10000 ); // 10 second timeout
try {
const stream = await renderToReadableStream ( < App /> , {
signal: controller . signal ,
onError ( error ) {
if ( error . name === 'AbortError' ) {
console . log ( 'Render aborted' );
} else {
console . error ( 'Render error:' , error );
}
},
});
clearTimeout ( timeout );
return new Response ( stream , {
headers: { 'Content-Type' : 'text/html' },
});
} catch ( error ) {
clearTimeout ( timeout );
throw error ;
}
}
Waiting for all content (SEO)
Wait for allReady when serving content to search engine crawlers:
import { renderToReadableStream } from 'react-dom/server' ;
function isCrawler ( userAgent ) {
return /bot | crawler | spider | crawling/ i . test ( userAgent );
}
export default async function handler ( request ) {
const stream = await renderToReadableStream ( < App /> , {
bootstrapScripts: [ '/client.js' ],
});
const userAgent = request . headers . get ( 'User-Agent' ) || '' ;
if ( isCrawler ( userAgent )) {
// Wait for all Suspense boundaries to resolve
await stream . allReady ;
}
return new Response ( stream , {
headers: { 'Content-Type' : 'text/html' },
});
}
import { renderToReadableStream } from 'react-dom/server' ;
export default async function handler ( request ) {
const responseHeaders = new Headers ();
const stream = await renderToReadableStream ( < App /> , {
onHeaders ( headers ) {
// Merge preload hints into response headers
headers . forEach (( value , key ) => {
responseHeaders . append ( key , value );
});
},
});
responseHeaders . set ( 'Content-Type' , 'text/html' );
return new Response ( stream , {
headers: responseHeaders ,
});
}
Advanced Patterns
import { renderToReadableStream } from 'react-dom/server' ;
export default async function handler ( request ) {
const stream = await renderToReadableStream ( < App /> );
// Transform the stream (e.g., inject analytics)
const transformedStream = stream . pipeThrough (
new TransformStream ({
transform ( chunk , controller ) {
const text = new TextDecoder (). decode ( chunk );
// Inject script before </body>
const transformed = text . replace (
'</body>' ,
'<script src="/analytics.js"></script></body>'
);
controller . enqueue ( new TextEncoder (). encode ( transformed ));
},
})
);
return new Response ( transformedStream , {
headers: { 'Content-Type' : 'text/html' },
});
}
Streaming with compression
import { renderToReadableStream } from 'react-dom/server' ;
export default async function handler ( request ) {
const stream = await renderToReadableStream ( < App /> );
// Check if client accepts compression
const acceptEncoding = request . headers . get ( 'Accept-Encoding' ) || '' ;
if ( acceptEncoding . includes ( 'gzip' )) {
// Compress the stream
const compressedStream = stream . pipeThrough (
new CompressionStream ( 'gzip' )
);
return new Response ( compressedStream , {
headers: {
'Content-Type' : 'text/html' ,
'Content-Encoding' : 'gzip' ,
},
});
}
return new Response ( stream , {
headers: { 'Content-Type' : 'text/html' },
});
}
Streaming HTML with document wrapper
import { renderToReadableStream } from 'react-dom/server' ;
class HTMLStream {
static async create ( element , options = {}) {
const { title = 'My App' , ... streamOptions } = options ;
// Render just the app content
const appStream = await renderToReadableStream ( element , streamOptions );
// Create a composite stream with HTML wrapper
const { readable , writable } = new TransformStream ();
const writer = writable . getWriter ();
// Write HTML opening
await writer . write (
new TextEncoder (). encode (
`<!DOCTYPE html><html><head><meta charset="utf-8"><title> ${ title } </title></head><body><div id="root">`
)
);
// Pipe app content
appStream . pipeTo ( writable , { preventClose: true }). then ( async () => {
// Write HTML closing
await writer . write (
new TextEncoder (). encode ( '</div></body></html>' )
);
await writer . close ();
});
return readable ;
}
}
// Usage
export default async function handler () {
const stream = await HTMLStream . create ( < App /> , {
title: 'My Streaming App' ,
bootstrapScripts: [ '/client.js' ],
});
return new Response ( stream , {
headers: { 'Content-Type' : 'text/html' },
});
}
Runtime Differences
// react-dom/server.edge
import { renderToReadableStream } from 'react-dom/server.edge' ;
// Uses native Web Streams (ReadableStreamController)
const stream = await renderToReadableStream ( < App /> );
// Returns: ReadableStream with direct controller access
Optimized for:
Vercel Edge Functions
Cloudflare Workers
Deno Deploy
Edge runtimes without Node.js APIs
// react-dom/server.node
import { renderToReadableStream } from 'react-dom/server.node' ;
// Wraps Node.js streams as Web Streams
const stream = await renderToReadableStream ( < App /> );
// Returns: ReadableStream (backed by Node.js Writable)
For Node.js, prefer renderToPipeableStream for better performance. // react-dom/server.bun
import { renderToReadableStream } from 'react-dom/server.bun' ;
// Optimized for Bun's native streams
const stream = await renderToReadableStream ( < App /> );
Bun provides optimized stream handling for better performance.
Common Issues
Stream already locked/consumed
ReadableStream can only be read once. Don’t try to use the same stream multiple times:const stream = await renderToReadableStream ( < App /> );
// This works
return new Response ( stream );
// This will fail - stream already consumed
const clone = stream . tee (); // Error!
Solution : Generate a new stream for each request.
Headers already sent error
Shell rendering fails with rejected Promise
If rendering fails before the shell is ready, the Promise rejects: try {
const stream = await renderToReadableStream ( < App /> );
return new Response ( stream );
} catch ( error ) {
// Shell failed to render - show error page
return new Response ( '<h1>Error</h1>' , { status: 500 });
}
If a Suspense boundary never resolves, allReady will hang: // This will hang forever
function NeverResolves () {
throw new Promise (() => {}); // Never resolves!
}
< Suspense fallback = { < div > Loading... </ div > } >
< NeverResolves />
</ Suspense >
Solution : Add timeouts and proper error handling for async operations.
Stream immediately for best TTFB
Use progressive chunk size
Control streaming granularity with progressiveChunkSize: const stream = await renderToReadableStream ( < App /> , {
progressiveChunkSize: 2048 , // bytes
});
Smaller = more granular streaming, larger = fewer chunks
Avoid blocking in Suspense boundaries
Keep Suspense boundaries small and focused: // Bad - entire page waits for slow data
< Suspense fallback = { < PageSpinner /> } >
< EntirePage /> { /* Includes slow data */ }
</ Suspense >
// Good - only slow parts wait
< Header /> { /* Streamed immediately */ }
< Suspense fallback = { < Spinner /> } >
< SlowData /> { /* Only this waits */ }
</ Suspense >
< Footer /> { /* Streamed immediately */ }
See Also