APIs for prerendering React components to static HTML with support for postponed content, enabling hybrid static and dynamic rendering strategies.
React DOM provides runtime-specific static generation implementations:
react-dom/static or react-dom/static.nodereact-dom/static.browserreact-dom/static.edgePrerenders React elements to a ReadableStream with optional postponed state for later completion.
/**
* Prerender to static HTML with postponed content support
* @param children - React elements to prerender
* @param options - Configuration options
* @returns Promise with prelude stream and postponed state
*/
function prerender(
children: ReactNode,
options?: PrerenderOptions
): Promise<{
prelude: ReadableStream;
postponed: PostponedState | null;
}>;
interface PrerenderOptions {
/** Prefix for IDs generated by useId */
identifierPrefix?: string;
/** Namespace URI for SVG or MathML content */
namespaceURI?: 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 prerendering */
signal?: AbortSignal;
/** Error handler called for all errors */
onError?: (error: Error, errorInfo: ErrorInfo) => string | void;
/** Called when content is postponed */
onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void;
/** External runtime script for React */
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor;
/** Import map for module resolution */
importMap?: ImportMap;
/** Called with headers that should be set */
onHeaders?: (headers: Headers) => void;
/** Maximum length for header values */
maxHeadersLength?: number;
}
interface BootstrapScriptDescriptor {
src: string;
integrity?: string;
crossOrigin?: 'anonymous' | 'use-credentials';
}
type PostponedState = OpaquePostponedState; // Opaque postponed rendering state
interface ErrorInfo {
componentStack?: string;
}
interface PostponeInfo {
componentStack?: string;
}Usage Examples:
import { prerender } from 'react-dom/static';
// Basic prerendering
const { prelude, postponed } = await prerender(<App />);
// Save to file
const file = await Deno.open('index.html', { write: true, create: true });
await prelude.pipeTo(file.writable);
// Check if anything was postponed
if (postponed) {
console.log('Some content was postponed');
// Save postponed state for later
await Deno.writeTextFile('postponed.json', JSON.stringify(postponed));
}
// With configuration
const { prelude, postponed } = await prerender(<App />, {
bootstrapModules: ['/app.js'],
signal: AbortSignal.timeout(30000),
onError(error, errorInfo) {
console.error('Prerender error:', error);
console.error('Component stack:', errorInfo.componentStack);
},
onPostpone(reason, postponeInfo) {
console.log('Postponed:', reason);
console.log('At:', postponeInfo.componentStack);
}
});
// Edge function for static generation
export default {
async scheduled(event, env) {
const { prelude, postponed } = await prerender(<BlogIndex />, {
bootstrapModules: ['/blog.js']
});
// Store static HTML
await env.STORAGE.put('blog/index.html', prelude);
// Store postponed state if needed
if (postponed) {
await env.STORAGE.put('blog/postponed.json', JSON.stringify(postponed));
}
}
};Key Features:
Node.js-specific prerendering to a Readable stream.
/**
* Prerender to Node.js Readable stream
* @param children - React elements to prerender
* @param options - Configuration options
* @returns Promise with prelude stream and postponed state
*/
function prerenderToNodeStream(
children: ReactNode,
options?: PrerenderOptions
): Promise<{
prelude: Readable;
postponed: PostponedState | null;
}>;
type Readable = NodeJS.ReadableStream;Usage Examples:
import { prerenderToNodeStream } from 'react-dom/static';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
// Prerender and save to file
const { prelude, postponed } = await prerenderToNodeStream(<App />);
await pipeline(
prelude,
createWriteStream('dist/index.html')
);
if (postponed) {
await writeFile(
'dist/postponed.json',
JSON.stringify(postponed)
);
}
// Build-time static generation
import { prerenderToNodeStream } from 'react-dom/static';
import { writeFile } from 'fs/promises';
async function buildStaticSite() {
const pages = [
{ path: 'index.html', component: <HomePage /> },
{ path: 'about.html', component: <AboutPage /> },
{ path: 'blog/index.html', component: <BlogIndex /> }
];
for (const page of pages) {
const { prelude, postponed } = await prerenderToNodeStream(
page.component,
{
bootstrapModules: ['/client.js'],
identifierPrefix: page.path + '-'
}
);
// Save HTML
const chunks = [];
for await (const chunk of prelude) {
chunks.push(chunk);
}
await writeFile(`dist/${page.path}`, Buffer.concat(chunks));
// Save postponed state
if (postponed) {
await writeFile(
`dist/${page.path}.postponed`,
JSON.stringify(postponed)
);
}
}
}
buildStaticSite();Resumes and prerenders from previously postponed state.
/**
* Resume and prerender from postponed state
* @param children - React elements to render
* @param postponedState - State from previous postponed render
* @param options - Configuration options
* @returns Promise with prelude stream and new postponed state
*/
function resumeAndPrerender(
children: ReactNode,
postponedState: PostponedState,
options?: PrerenderOptions
): Promise<{
prelude: ReadableStream;
postponed: PostponedState | null;
}>;Usage Example:
import { resumeAndPrerender } from 'react-dom/static';
// Load postponed state from build time
const postponedState = await loadPostponedState();
export default {
async fetch(request) {
// Complete the postponed rendering at request time
const { prelude, postponed } = await resumeAndPrerender(
<App url={request.url} />,
postponedState,
{
bootstrapModules: ['/app.js'],
onError(error) {
console.error('Resume error:', error);
}
}
);
// Should be no more postponed content
if (postponed) {
console.warn('Still have postponed content');
}
return new Response(prelude, {
headers: { 'Content-Type': 'text/html' }
});
}
};Node.js-specific resume and prerender from postponed state.
/**
* Resume and prerender to Node.js stream from postponed state
* @param children - React elements to render
* @param postponedState - State from previous postponed render
* @param options - Configuration options
* @returns Promise with prelude stream and new postponed state
*/
function resumeAndPrerenderToNodeStream(
children: ReactNode,
postponedState: PostponedState,
options?: PrerenderOptions
): Promise<{
prelude: Readable;
postponed: PostponedState | null;
}>;Usage Example:
import { resumeAndPrerenderToNodeStream } from 'react-dom/static';
import { readFile } from 'fs/promises';
// Request handler completing postponed render
export async function handler(req, res) {
// Load postponed state from build
const postponedState = JSON.parse(
await readFile('dist/postponed.json', 'utf-8')
);
const { prelude, postponed } = await resumeAndPrerenderToNodeStream(
<App url={req.url} />,
postponedState,
{
bootstrapModules: ['/app.js']
}
);
res.setHeader('Content-Type', 'text/html');
prelude.pipe(res);
}import { postpone } from 'react';
function DynamicContent({ userId }) {
if (typeof window === 'undefined') {
// Postpone during static generation
postpone('User-specific content');
}
// This runs at request time or on client
const user = useUser(userId);
return <div>{user.name}</div>;
}
function BlogPost() {
return (
<article>
<h1>Static Title</h1>
<p>Static content that gets prerendered...</p>
{/* This will be postponed */}
<DynamicContent userId={currentUserId} />
</article>
);
}Workflow:
prerender() - static content is rendered, dynamic content is postponedresumeAndPrerender() with the postponed state - dynamic content is renderedimport { Suspense, postpone } from 'react';
function UserProfile({ userId }) {
if (typeof window === 'undefined') {
postpone('User profile');
}
const user = useUser(userId);
return <ProfileCard user={user} />;
}
function App() {
return (
<div>
<Header /> {/* Static */}
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId={currentUserId} />
</Suspense>
<Footer /> {/* Static */}
</div>
);
}
// Build time:
// - Header and Footer are in static HTML
// - UserProfile is in postponed state with Suspense fallback
// Request time:
// - UserProfile completes
// - Client hydrates seamlesslyNo postponed content - everything is static.
const { prelude, postponed } = await prerenderToNodeStream(<StaticPage />);
console.log(postponed); // null
// Save to CDN
await saveToS3('page.html', prelude);Static shell with dynamic content.
// Build time
const { prelude, postponed } = await prerenderToNodeStream(
<BlogPost id={postId} />
);
await saveStatic('blog/' + postId + '.html', prelude);
await savePostponed('blog/' + postId + '.postponed', postponed);
// Request time
const postponedState = await loadPostponed('blog/' + postId + '.postponed');
const { prelude } = await resumeAndPrerenderToNodeStream(
<BlogPost id={postId} currentUser={req.user} />,
postponedState
);
res.send(prelude);Build popular pages statically, others on-demand.
// Build popular pages
for (const id of popularPostIds) {
const { prelude } = await prerenderToNodeStream(<Post id={id} />);
await saveToCDN(`posts/${id}.html`, prelude);
}
// On-demand for other pages
export async function handler(req) {
const id = req.params.id;
if (await existsInCDN(`posts/${id}.html`)) {
return serveFromCDN(`posts/${id}.html`);
}
// Generate on first request
const { prelude } = await prerenderToNodeStream(<Post id={id} />);
// Cache for future requests
await saveToCDN(`posts/${id}.html`, prelude);
return new Response(prelude);
}Regenerate static pages periodically.
const cache = new Map();
export async function handler(req) {
const path = req.url.pathname;
const cached = cache.get(path);
// Serve from cache if fresh
if (cached && Date.now() - cached.timestamp < 60000) {
return new Response(cached.html);
}
// Regenerate
const { prelude } = await prerenderToNodeStream(<App path={path} />);
const chunks = [];
for await (const chunk of prelude) {
chunks.push(chunk);
}
const html = Buffer.concat(chunks).toString();
// Update cache
cache.set(path, { html, timestamp: Date.now() });
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
});
}const { prelude, postponed } = await prerenderToNodeStream(<App />, {
onError(error, errorInfo) {
// Log errors during prerendering
console.error('Prerender error:', error);
console.error('Stack:', errorInfo.componentStack);
// Optionally include error HTML
return `<!-- Error: ${error.message} -->`;
}
});const { prelude, postponed } = await prerenderToNodeStream(<App />, {
onPostpone(reason, postponeInfo) {
console.log('Content postponed:', reason);
console.log('Location:', postponeInfo.componentStack);
// Track what's being postponed
metrics.increment('postponed_content', {
reason,
location: postponeInfo.componentStack
});
}
});try {
const { prelude, postponed } = await resumeAndPrerender(
<App />,
postponedState,
{
signal: AbortSignal.timeout(5000),
onError(error) {
console.error('Resume error:', error);
}
}
);
if (postponed) {
console.warn('Still have postponed content after resume');
}
return new Response(prelude);
} catch (error) {
console.error('Resume failed:', error);
return new Response('Error', { status: 500 });
}// ❌ Bad: Postponing everything
function Page() {
if (typeof window === 'undefined') {
postpone('entire page');
}
return <App />;
}
// ✅ Good: Only postpone what's necessary
function Page() {
return (
<div>
<StaticHeader />
<StaticContent />
<DynamicUserSection /> {/* Only this postpones */}
<StaticFooter />
</div>
);
}// Generate multiple pages in parallel
await Promise.all(
pages.map(async (page) => {
const { prelude, postponed } = await prerenderToNodeStream(page.component);
await savePage(page.path, prelude, postponed);
})
);import { S3 } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
const { prelude } = await prerenderToNodeStream(<App />);
const upload = new Upload({
client: s3,
params: {
Bucket: 'static-site',
Key: 'index.html',
Body: prelude,
ContentType: 'text/html'
}
});
await upload.done();| Feature | Static Generation | Server Rendering |
|---|---|---|
| When | Build time | Request time |
| Speed | Instant (pre-built) | Fast (streamed) |
| Personalization | Limited (with postpone) | Full |
| Server Load | None (CDN) | Per request |
| SEO | Excellent | Excellent |
| Freshness | Stale (until rebuild) | Always fresh |
| Cost | Low (static hosting) | Higher (server) |
// Build entire docs site statically
const docs = await loadAllDocs();
for (const doc of docs) {
const { prelude } = await prerenderToNodeStream(
<DocPage content={doc.content} />
);
await saveToStatic(`docs/${doc.slug}.html`, prelude);
}// Static product details, dynamic cart
function ProductPage({ productId }) {
return (
<div>
<ProductDetails id={productId} /> {/* Static */}
<Reviews id={productId} /> {/* Static */}
<UserCart /> {/* Postponed */}
</div>
);
}// Static post content, dynamic comments
function BlogPost({ postId }) {
return (
<article>
<PostContent id={postId} /> {/* Static */}
<Comments postId={postId} /> {/* Postponed or client-only */}
</article>
);
}const version: string; // "19.2.0"interface ImportMap {
imports?: Record<string, string>;
scopes?: Record<string, Record<string, string>>;
}interface Headers {
[key: string]: string | string[];
}