or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client-rendering.mdform-actions.mdindex.mdresource-hints.mdserver-rendering.mdstatic-generation.md
tile.json

server-rendering.mddocs/

Server-Side Rendering

Streaming server-side rendering APIs optimized for different JavaScript runtimes with full support for Suspense, Server Components, and progressive hydration.

Runtime Selection

React DOM automatically selects the appropriate runtime implementation:

  • Node.js: react-dom/server or react-dom/server.node
  • Browser/Deno: react-dom/server.browser
  • Edge Runtimes: react-dom/server.edge (Cloudflare Workers, Vercel Edge)
  • Bun: react-dom/server.bun

Import from react-dom/server for automatic selection, or use runtime-specific entry points for explicit control.

Capabilities

Node.js Streaming - renderToPipeableStream

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
}

Web Streams - renderToReadableStream

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:

  • Returns a Promise that resolves when initial shell is ready
  • The stream has an allReady property that resolves when all Suspense boundaries are done
  • Ideal for edge runtimes and modern JavaScript environments
  • Use signal option with AbortSignal for timeout control

Resume Rendering - resumeToPipeableStream

Resumes 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 prerender

Usage 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:

  • Completing partial static renders at request time
  • Hybrid static + dynamic rendering strategies
  • Postponing expensive computations until request time

Resume to ReadableStream - resume

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' }
    });
  }
};

Static Prerendering from Server Entry Points

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.

Legacy APIs (Deprecated)

renderToString

/**
 * 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:

  • Blocks the server thread (synchronous)
  • No Suspense support
  • No streaming
  • Slower time to first byte (TTFB)
  • All async data must be loaded before rendering starts

Migration:

// Old
const html = renderToString(<App />);
res.send(html);

// New
const stream = renderToPipeableStream(<App />, {
  onShellReady() {
    stream.pipe(res);
  }
});

renderToStaticMarkup

/**
 * 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:

  • Generating static HTML emails
  • Creating static pages without hydration

Why Deprecated:

  • Same issues as renderToString
  • Output cannot be hydrated

Migration:

For static HTML generation, use the static prerendering APIs from react-dom/static instead.

Suspense and Streaming

How Streaming Works

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:

  • Fast initial page load (shell renders first)
  • Progressive content loading
  • Better perceived performance
  • Parallel data fetching

Selective Hydration

import { Suspense } from 'react';

function App() {
  return (
    <div>
      <NavBar />
      <Suspense fallback={<Spinner />}>
        <Comments />
      </Suspense>
      <Suspense fallback={<Sidebar />}>
        <RightColumn />
      </Suspense>
    </div>
  );
}

Hydration Order:

  1. NavBar hydrates first (no Suspense)
  2. Comments and RightColumn hydrate as they stream in
  3. User interactions prioritize hydration of that component
  4. Non-interactive sections can hydrate last

Error Handling Strategies

Graceful Degradation

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>');
  }
});

Error Boundaries

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>
  );
}

Bootstrap Scripts

Loading Client JavaScript

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>

Best Practices

  1. Use onShellReady for users: Start streaming as soon as the shell is ready for better perceived performance
  2. Use onAllReady for crawlers: Wait for all content for SEO-critical pages
  3. Configure onError: Always log errors even if streaming continues
  4. Handle onShellError: Provide fallback HTML when the shell fails
  5. Use Suspense boundaries: Break up async dependencies for progressive loading
  6. Configure bootstrapScripts: Ensure client-side hydration code loads
  7. Set proper identifierPrefix: Avoid ID collisions with multiple React roots
  8. Use abort for timeouts: Prevent indefinite hanging with abort()
  9. Handle postpone: Implement onPostpone for static generation workflows
  10. Monitor TTFB: Optimize shell content to minimize time to first byte

Runtime-Specific Notes

Node.js

  • Use renderToPipeableStream for best performance
  • Supports back-pressure with pipe()
  • Can use onHeaders for setting HTTP headers

Browser/Deno

  • Use renderToReadableStream
  • Works with standard Web Streams API
  • Compatible with Service Workers

Edge Runtimes

  • Use react-dom/server.edge
  • Optimized for Cloudflare Workers, Vercel Edge
  • Limited execution time - handle with signal

Bun

  • Use react-dom/server.bun
  • Native ReadableStream support
  • High performance rendering

Version

const version: string; // "19.2.0"

Types

ImportMap

interface ImportMap {
  imports?: Record<string, string>;
  scopes?: Record<string, Record<string, string>>;
}

ReactFormState

interface ReactFormState<S = any, P = any> {
  [key: string]: any;
}

Headers

interface Headers {
  [key: string]: string | string[];
}