ssr & streaming

[check: …]
📖 Read the full guide — SSR, streaming with Suspense, renderToString vs renderToPipeableStream, and the render→flush→hydrate flow. This page is the interactive companion.

The one idea

SSR renders the React tree to HTML on the server for instant first paint + SEO. STREAMING flushes that HTML as it's ready — so a slow <Suspense> boundary stops blocking the rest of the page. The user sees the fast shell now; the slow chunk streams in later in the same response.

Non-streaming waits for all data, then sends one blob: first paint == full load. Streaming sends the ready shell first and keeps the connection open, appending chunks as each Suspense boundary resolves. Hydration (hydrateRoot) then attaches interactivity to whatever HTML has arrived.

renderToPipeableStream + Suspense = streaming renderToString = one blob, no streaming hydrateRoot = client takes over server HTML

The visualizer — fast shell + slow Suspense chunk

A page has a fast part (header + nav, ready at 100 ms) and a slow part (a Suspense boundary waiting on data that takes delay ms). Toggle streaming, drag the delay, and watch first paint vs full load diverge or merge.

1. rendering mode

2000 ms

Page preview (simulated)

localhost:3000/dashboard
t = 0 ms
▎ Acme Dashboard
⏳ loading slow data… (Suspense fallback)

Metrics for current mode

Non-streaming vs streaming — the timelines

Both timelines recompute from the same delay. The one matching your toggle is lit; the other is dimmed for contrast. ▲ first paint is when bytes first hit the browser; ● fully loaded is when the last chunk lands.

NON-STREAMING renderToString whole page waits
0 ms
STREAMING renderToPipeableStream + Suspense shell now, slow chunk later
0 ms

Render → flush → hydrate (how Start orchestrates it)

Start wires this through Nitro/Vite. The server entry picks a handler: defaultRenderHandler / renderRouterToString (non-streaming) or defaultStreamHandler / renderRouterToStream (streaming). Loader/server-function data is auto-dehydrated into the HTML and rehydrated on the client.

1 · server
RENDER
// React tree -> HTML
import { renderRouterToStream }
  from '@tanstack/react-router/ssr/server'
// under the hood: React's
// renderToPipeableStream(<App/>)
2 · wire
FLUSH / STREAM
// onShellReady: send the fast shell
//   -> first paint (early!)
// stream stays OPEN; each resolved
// <Suspense> appends a chunk
3 · client
HYDRATE
import { hydrateRoot }
  from 'react-dom/client'
hydrateRoot(document,
  <RouterClient router={router} />)
// attaches events; selective
// hydration per chunk

Comparison — mode × behavior

This bundle is HOW TanStack Start turns React into server HTML and streams it: renderRouterToStream + Suspense flushes the shell early; hydrateRoot finishes the job on the client. Cross-refs: 🔗 server_functions (where server data comes from under SSR) · 🔗 astro_rendering_modes (the rendering-mode choice) · 🔗 spa_vs_mpa (SPA vs SSR/MPA — TanStack spans both).