We use cookies to enhance your experience on the site
CodeWorlds

Streaming SSR and Edge Runtime

Imagine that instead of sending the entire spaceship in one transport, you send it module by module -- the cockpit immediately, while navigation systems and cargo bays arrive in the background. That is exactly how Streaming SSR works -- the server starts sending HTML to the browser before the entire rendering is complete. And Edge Runtime moves this process closer to the user -- like relay stations distributed across the galaxy.

Traditional SSR vs Streaming SSR

Traditional SSR generates the entire HTML on the server before sending it to the browser. The user waits for the slowest component:

1// Traditional SSR - user waits for EVERYTHING
2// Server: [render Header][render Data][render Footer] -> send everything
3// Time: 500ms + 2000ms + 100ms = 2600ms before they see anything
4
5// Streaming SSR - fragments sent immediately
6// Server: [Header ready] -> send! [Data ready] -> send! [Footer] -> send!
7// Time: 500ms to first fragment, rest arrives in background

renderToPipeableStream - Streaming API

React 18 introduces

renderToPipeableStream
as the successor to
renderToString
:

1import { renderToPipeableStream } from 'react-dom/server';
2
3// Node.js server with Streaming SSR
4app.get('/', (req, res) => {
5  const { pipe } = renderToPipeableStream(
6    <App />,
7    {
8      bootstrapScripts: ['/main.js'],
9      onShellReady() {
10        // Shell is ready - send immediately
11        res.setHeader('Content-Type', 'text/html');
12        pipe(res);
13      },
14      onShellError(error) {
15        // Error in critical part - send fallback
16        res.statusCode = 500;
17        res.send('<h1>Server Error</h1>');
18      },
19      onAllReady() {
20        // Everything ready (useful for crawlers/SEO)
21      },
22      onError(error) {
23        console.error('Streaming error:', error);
24      }
25    }
26  );
27});

onShellReady
is called when the main "shell" of the page is ready -- everything outside
<Suspense>
boundaries. The user sees the page skeleton immediately.

renderToReadableStream - Edge Runtime API

For Edge Runtime (Cloudflare Workers, Vercel Edge, Deno Deploy) we use

renderToReadableStream
, which returns a Web Streams API:

1import { renderToReadableStream } from 'react-dom/server';
2
3// Edge Runtime handler
4export default async function handler(request) {
5  const stream = await renderToReadableStream(
6    <App />,
7    {
8      bootstrapScripts: ['/main.js'],
9      onError(error) {
10        console.error('Edge streaming error:', error);
11      }
12    }
13  );
14
15  return new Response(stream, {
16    headers: { 'Content-Type': 'text/html' },
17  });
18}

Difference between APIs:

  • renderToPipeableStream
    - Node.js Streams (Node.js runtime)
  • renderToReadableStream
    - Web Streams API (Edge Runtime, Deno, browsers)

Suspense as Streaming Boundaries

<Suspense>
defines streaming boundaries -- the server sends a fallback and later replaces it with the ready HTML:

1function SpaceStationDashboard() {
2  return (
3    <div>
4      {/* Rendered immediately - part of "shell" */}
5      <Header title="Space Station Alpha" />
6      <Navigation />
7
8      {/* Each section streams independently */}
9      <Suspense fallback={<Skeleton type="missions" />}>
10        <MissionList />  {/* Data loads in 800ms */}
11      </Suspense>
12
13      <Suspense fallback={<Skeleton type="crew" />}>
14        <CrewStatus />   {/* Data loads in 1500ms */}
15      </Suspense>
16
17      <Suspense fallback={<Skeleton type="telemetry" />}>
18        <TelemetryData />  {/* Data loads in 2200ms */}
19      </Suspense>
20    </div>
21  );
22}
23
24// Streaming flow:
25// 1. [0ms]    Server sends: Header + Navigation + 3x Skeleton
26// 2. [800ms]  Server streams: MissionList (replaces Skeleton)
27// 3. [1500ms] Server streams: CrewStatus (replaces Skeleton)
28// 4. [2200ms] Server streams: TelemetryData (replaces Skeleton)

Edge Runtime - Computations Closer to the User

Edge Runtime is an execution environment running on the "edge" of the network -- close to the user, on globally distributed CDN servers. Instead of one central server, your application runs in hundreds of locations:

1// next.config.js - Edge Runtime configuration in Next.js
2// In the page file:
3export const runtime = 'edge';
4
5// Or in a route handler:
6export const runtime = 'edge';
7
8export async function GET(request) {
9  // This code executes on the edge (close to user)
10  const userLocation = request.geo?.city || 'Unknown';
11
12  return new Response(
13    JSON.stringify({ message: 'Hello from edge!', location: userLocation }),
14    { headers: { 'Content-Type': 'application/json' } }
15  );
16}

Differences: Node.js vs Edge Runtime

| Feature | Node.js Runtime | Edge Runtime | |---------|----------------|--------------| | Location | Single server region | Globally distributed | | Cold start | 200-500ms | 1-5ms | | Bundle size | No limit | ~1-4MB limit | | API | Full Node.js API | Limited (Web APIs) | | Streaming | renderToPipeableStream | renderToReadableStream | | Databases | Any | Edge-compatible (Planetscale, Turso) |

Progressive Hydration

Streaming SSR naturally supports progressive hydration -- React hydrates components as they appear:

1// Progressive hydration flow:
2// 1. Server streams HTML -> browser displays
3// 2. JavaScript loads
4// 3. React hydrates components already in DOM
5// 4. New HTML fragments arrive from stream
6// 5. React hydrates them immediately upon receipt
7
8// Selective hydration - React prioritizes
9// components the user interacts with:
10function App() {
11  return (
12    <div>
13      {/* User clicked here - React hydrates this FIRST */}
14      <Suspense fallback={<Skeleton />}>
15        <InteractivePanel />
16      </Suspense>
17
18      {/* This waits in queue */}
19      <Suspense fallback={<Skeleton />}>
20        <HeavyDataTable />
21      </Suspense>
22    </div>
23  );
24}

Summary

Streaming SSR and Edge Runtime are powerful tools for accelerating application delivery:

  1. Streaming SSR - sends HTML in fragments, user sees the page immediately
  2. renderToPipeableStream - Node.js API for streaming
  3. renderToReadableStream - Edge Runtime API (Web Streams)
  4. Suspense boundaries - define streaming granularity
  5. Edge Runtime - code execution closer to the user (low latency, fast cold start)
  6. Progressive Hydration - React hydrates components as they appear

Streaming SSR changes application delivery like modular systems on a space station -- each module launches independently, the user does not wait for the whole thing.

Go to CodeWorlds