Streaming server-side rendering APIs optimized for different JavaScript runtimes with full support for Suspense, Server Components, and progressive hydration.
React DOM automatically selects the appropriate runtime implementation:
react-dom/server or react-dom/server.nodereact-dom/server.browserreact-dom/server.edge (Cloudflare Workers, Vercel Edge)react-dom/server.bunImport from react-dom/server for automatic selection, or use runtime-specific entry points for explicit control.
Renders React elements to a pipeable Node.js stream for server-side rendering with Suspense support.
/**
* Render to Node.js pipeable stream (primary SSR API for Node.js)
* @param children - React elements to render
* @param options - Configuration options
* @returns PipeableStream object
*/
function renderToPipeableStream(
children: ReactNode,
options?: RenderToPipeableStreamOptions
): PipeableStream;
interface RenderToPipeableStreamOptions {
/** Prefix for IDs generated by useId */
identifierPrefix?: string;
/** Namespace URI for SVG or MathML content */
namespaceURI?: string;
/** CSP nonce for inline scripts and styles */
nonce?: string | { script?: string; style?: string };
/** Inline script content to inject before React code */
bootstrapScriptContent?: string;
/** External script URLs to load */
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>;
/** ES module URLs to load */
bootstrapModules?: Array<string | BootstrapScriptDescriptor>;
/** Target chunk size for progressive streaming (bytes) */
progressiveChunkSize?: number;
/** Called when initial shell is ready to be sent */
onShellReady?: () => void;
/** Called if shell rendering encounters an error */
onShellError?: (error: Error) => void;
/** Called when all content including Suspense is ready */
onAllReady?: () => void;
/** Error handler called for all errors */
onError?: (error: Error, errorInfo: ErrorInfo) => string | void;
/** Called when rendering is postponed (static generation) */
onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void;
/** External runtime script for React */
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor;
/** Import map for module resolution */
importMap?: ImportMap;
/** Form state for Server Actions */
formState?: ReactFormState;
/** Called with headers that should be set */
onHeaders?: (headers: Headers) => void;
/** Maximum length for header values */
maxHeadersLength?: number;
}
interface PipeableStream {
/** Pipe to a Node.js writable stream */
pipe<T extends Writable>(destination: T): T;
/** Abort rendering and streaming */
abort(reason?: string): void;
}
interface BootstrapScriptDescriptor {
src: string;
integrity?: string;
crossOrigin?: 'anonymous' | 'use-credentials';
}
interface ErrorInfo {
componentStack?: string;
}
interface PostponeInfo {
componentStack?: string;
}Usage Examples:
import { renderToPipeableStream } from 'react-dom/server';
import { createServer } from 'http';
// Basic usage
createServer((req, res) => {
const stream = renderToPipeableStream(<App />, {
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
stream.pipe(res);
},
onError(error) {
console.error(error);
res.statusCode = 500;
res.end('Internal Server Error');
}
});
}).listen(3000);
// With bootstrapping and error handling
const stream = renderToPipeableStream(<App url={req.url} />, {
bootstrapScripts: ['/client.js'],
bootstrapModules: ['/app.js'],
identifierPrefix: 'app-',
onShellReady() {
// Shell (above Suspense boundaries) is ready
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
stream.pipe(res);
},
onShellError(error) {
// Error in initial shell before Suspense
res.statusCode = 500;
res.setHeader('Content-Type', 'text/html');
res.send('<h1>Server Error</h1>');
},
onError(error, errorInfo) {
// Log errors but continue streaming
console.error('SSR Error:', error);
console.error('Component stack:', errorInfo.componentStack);
}
});
// Wait for all content (including Suspense)
const stream = renderToPipeableStream(<App />, {
onAllReady() {
// Everything including Suspense boundaries is ready
// Use for crawlers/bots that need complete content
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
stream.pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.end();
}
});
// Aborting
const stream = renderToPipeableStream(<App />, options);
setTimeout(() => {
stream.abort('Timeout');
}, 10000);Streaming Strategy:
// For users: Stream shell immediately, lazy-load rest
onShellReady() {
stream.pipe(res); // Fast initial response
}
// For bots: Wait for all content
onAllReady() {
stream.pipe(res); // Complete HTML for SEO
}Renders React elements to a Web Streams API ReadableStream for environments supporting the standard.
/**
* Render to Web Streams API ReadableStream
* @param children - React elements to render
* @param options - Configuration options
* @returns Promise resolving to ReadableStream with allReady property
*/
function renderToReadableStream(
children: ReactNode,
options?: RenderToReadableStreamOptions
): Promise<ReadableStream & { allReady: Promise<void> }>;
interface RenderToReadableStreamOptions {
/** Prefix for IDs generated by useId */
identifierPrefix?: string;
/** Namespace URI for SVG or MathML content */
namespaceURI?: string;
/** CSP nonce for inline scripts and styles */
nonce?: string | { script?: string; style?: string };
/** Inline script content to inject before React code */
bootstrapScriptContent?: string;
/** External script URLs to load */
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>;
/** ES module URLs to load */
bootstrapModules?: Array<string | BootstrapScriptDescriptor>;
/** Target chunk size for progressive streaming (bytes) */
progressiveChunkSize?: number;
/** AbortSignal to cancel rendering */
signal?: AbortSignal;
/** Error handler called for all errors */
onError?: (error: Error, errorInfo: ErrorInfo) => string | void;
/** Called when rendering is postponed */
onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void;
/** External runtime script for React */
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor;
/** Import map for module resolution */
importMap?: ImportMap;
/** Form state for Server Actions */
formState?: ReactFormState;
/** Called with headers that should be set */
onHeaders?: (headers: Headers) => void;
/** Maximum length for header values */
maxHeadersLength?: number;
}Usage Examples:
import { renderToReadableStream } from 'react-dom/server';
// Deno HTTP server
Deno.serve(async (req) => {
const stream = await renderToReadableStream(<App url={req.url} />, {
bootstrapModules: ['/app.js'],
onError(error) {
console.error('SSR Error:', error);
}
});
return new Response(stream, {
status: 200,
headers: { 'Content-Type': 'text/html' }
});
});
// Edge runtime (Cloudflare Workers, Vercel Edge)
export default {
async fetch(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapModules: ['/app.js']
});
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
}
};
// Wait for all content
const stream = await renderToReadableStream(<App />, {
signal: AbortSignal.timeout(10000)
});
// Wait for Suspense boundaries
await stream.allReady;
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
// With error handling
try {
const stream = await renderToReadableStream(<App />, {
onError(error, errorInfo) {
console.error('Render error:', error);
logToService(error, errorInfo.componentStack);
}
});
return new Response(stream, {
status: 200,
headers: { 'Content-Type': 'text/html' }
});
} catch (error) {
return new Response('<h1>Error</h1>', {
status: 500,
headers: { 'Content-Type': 'text/html' }
});
}Notes:
allReady property that resolves when all Suspense boundaries are donesignal option with AbortSignal for timeout controlResumes rendering from a postponed state (used in static site generation workflows).
/**
* Resume rendering to pipeable stream from postponed state
* @param children - React elements to render
* @param postponedState - State from previous postponed render
* @param options - Configuration options
* @returns PipeableStream object
*/
function resumeToPipeableStream(
children: ReactNode,
postponedState: PostponedState,
options?: RenderToPipeableStreamOptions
): PipeableStream;
type PostponedState = OpaquePostponedState; // Opaque type from prerenderUsage Example:
import { resumeToPipeableStream } from 'react-dom/server';
// Resume from postponed state (typically from static generation)
export function handler(req, res, postponedState) {
const stream = resumeToPipeableStream(
<App url={req.url} />,
postponedState,
{
bootstrapScripts: ['/client.js'],
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
stream.pipe(res);
},
onError(error) {
console.error('Resume error:', error);
}
}
);
}Use Cases:
Resumes rendering to ReadableStream from postponed state.
/**
* Resume rendering to ReadableStream from postponed state
* @param children - React elements to render
* @param postponedState - State from previous postponed render
* @param options - Configuration options
* @returns Promise resolving to ReadableStream
*/
function resume(
children: ReactNode,
postponedState: PostponedState,
options?: RenderToReadableStreamOptions
): Promise<ReadableStream & { allReady: Promise<void> }>;Usage Example:
import { resume } from 'react-dom/server';
// Edge function resuming from postponed state
export default {
async fetch(request, env) {
const postponedState = await env.KV.get('postponed-state', 'json');
const stream = await resume(<App />, postponedState, {
bootstrapModules: ['/app.js']
});
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
}
};Note: The prerender, resumeAndPrerender, prerenderToNodeStream, and resumeAndPrerenderToNodeStream functions are also exported from server entry points (react-dom/server.node, react-dom/server.browser, react-dom/server.edge) for convenience. These functions are primarily for static site generation workflows.
For complete documentation of these static prerendering APIs, see Static Site Generation.
/**
* Deprecated: Render to HTML string synchronously
* @param children - React elements to render
* @returns HTML string
* @deprecated Use renderToPipeableStream or renderToReadableStream instead
*/
function renderToString(children: ReactNode): string;Why Deprecated:
Migration:
// Old
const html = renderToString(<App />);
res.send(html);
// New
const stream = renderToPipeableStream(<App />, {
onShellReady() {
stream.pipe(res);
}
});/**
* Deprecated: Render to static HTML without React attributes
* @param children - React elements to render
* @returns HTML string without React internal attributes
* @deprecated Use streaming APIs for better performance
*/
function renderToStaticMarkup(children: ReactNode): string;Use Case:
Why Deprecated:
Migration:
For static HTML generation, use the static prerendering APIs from react-dom/static instead.
import { Suspense } from 'react';
function App() {
return (
<html>
<body>
<Header />
<Suspense fallback={<Spinner />}>
<Comments /> {/* Async data */}
</Suspense>
<Footer />
</body>
</html>
);
}
// Streaming sequence:
// 1. Send: <html><body><Header/><Spinner/>
// 2. When Comments ready: Send Comments + script to replace Spinner
// 3. Send: <Footer/></body></html>Benefits:
import { Suspense } from 'react';
function App() {
return (
<div>
<NavBar />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
<Suspense fallback={<Sidebar />}>
<RightColumn />
</Suspense>
</div>
);
}Hydration Order:
const stream = renderToPipeableStream(<App />, {
onError(error, errorInfo) {
// Log but continue streaming
console.error('Component error:', error);
logToMonitoring(error, errorInfo.componentStack);
// Return error message to include in HTML
return `<!-- Error: ${error.message} -->`;
},
onShellError(error) {
// Shell failed - send fallback HTML
res.statusCode = 500;
res.send('<h1>Something went wrong</h1>');
}
});import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong.</h2>;
}
return this.props.children;
}
}
// Use in SSR
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<AsyncContent />
</Suspense>
</ErrorBoundary>
);
}renderToPipeableStream(<App />, {
// Classic scripts (blocking)
bootstrapScripts: ['/static/js/client.js'],
// ES modules (non-blocking)
bootstrapModules: [
'/static/js/app.js',
{
src: '/static/js/vendor.js',
integrity: 'sha384-...',
crossOrigin: 'anonymous'
}
],
// Inline script
bootstrapScriptContent: 'window.ENV = "production"',
// External React runtime
unstable_externalRuntimeSrc: '/static/js/react-runtime.js'
});Output:
<!DOCTYPE html>
<html>
<head>
<script>window.ENV = "production"</script>
</head>
<body>
<div id="root">...</div>
<script src="/static/js/client.js"></script>
<script type="module" src="/static/js/app.js"></script>
<script
type="module"
src="/static/js/vendor.js"
integrity="sha384-..."
crossorigin="anonymous"
></script>
</body>
</html>abort()onPostpone for static generation workflowsrenderToPipeableStream for best performancepipe()onHeaders for setting HTTP headersrenderToReadableStreamreact-dom/server.edgesignalreact-dom/server.bunconst version: string; // "19.2.0"interface ImportMap {
imports?: Record<string, string>;
scopes?: Record<string, Record<string, string>>;
}interface ReactFormState<S = any, P = any> {
[key: string]: any;
}interface Headers {
[key: string]: string | string[];
}