Streaming SSR in Depth: Suspense, Selective Hydration & TTFB
AI generated
</>
{ }
React · Suspense · Streaming SSR · Performance
Streaming SSR in Depth
Suspense, Selective Hydration, and Time to First Byte

Streaming SSR splits a server response into several chunks instead of waiting for complete HTML before sending anything to the browser at all. React Suspense defines the boundaries where individual parts of a page are delivered later, while quickly available content becomes visible immediately.

19 min read Suspense · loading.tsx · Selective Hydration React 19 · Next.js App Router

1. What streaming SSR is and why it pays off

Streaming SSR solves a fundamental problem of classic server-side rendering: with traditional SSR, the server must have fully assembled the complete HTML document before even a single byte is sent to the browser. If a single data query is slow, say an external API or a complex database query, that one query blocks the entire page, even if ninety percent of the content has long been ready. Streaming SSR breaks this all-or-nothing principle by having the server deliver HTML in several chunks over an open HTTP connection, as soon as each part is ready.

The perceived speed gain of streaming SSR is substantial, because the user sees the top of a page, say the header and hero section, immediately, while product recommendations or comments are still loading in the background. The result is a page that feels faster, even if the total time to fully load stays identical. This exact effect makes streaming SSR one of the most effective tools for improving perceived performance without changing anything about the actual data load.

Technically, streaming SSR in React relies on the ability to write a response stream incrementally instead of returning a finished string. Next.js builds directly on this model with the App Router: every route can define its own Suspense boundaries where the server response gets segmented. The next chapter shows how React Suspense technically enables that segmentation.

2. React Suspense as the foundation of streaming SSR

Without <Suspense> boundaries, there is no streaming SSR in the React ecosystem. A Suspense component marks an area in the tree whose rendering is allowed to pause while an asynchronous operation, usually a data fetch, is still running. Instead of blocking the entire render, React immediately shows the fallback content of the Suspense boundary and delivers the actual content later, once the data is available, as a separate HTML chunk with a matching script to swap it into the DOM.

This fallback-then-replace logic is the core of streaming SSR. Multiple Suspense boundaries can resolve in parallel and independently of each other, which means a slow component does not wait for a faster one and vice versa. React sends the chunks in the order the data actually becomes available, not in the order they appear in the JSX tree. This property of streaming SSR fundamentally differs from classic SSR, where the order in the code exactly matches the delivery order.


// app/dashboard/page.tsx — independent Suspense boundaries stream separately
import { Suspense } from 'react';
import { UserProfile } from './user-profile';
import { RecentOrders } from './recent-orders';
import { Recommendations } from './recommendations';

export default function DashboardPage() {
  return (
    <div>
      {/* Fast: resolves almost immediately, arrives in the first chunk */}
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile />
      </Suspense>

      {/* Medium: independent boundary, streams whenever its data resolves */}
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>

      {/* Slow: does not block the two boundaries above */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
    </div>
  );
}

An important detail for streaming SSR with Suspense: the fallback must be able to render synchronously without its own data dependency, otherwise React cannot deliver it immediately. Skeleton components that only contain static markup with Tailwind classes for placeholder bars are the standard case. A nested Suspense boundary inside a fallback works technically, but it complicates the mental model of streaming considerably and should only be used deliberately.

3. loading.tsx and route-level streaming in Next.js

Next.js abstracts manual Suspense wiring at the route level with the loading.tsx convention. Place this file next to a page.tsx, and Next.js automatically wraps the entire page in a Suspense boundary whose fallback is the content of loading.tsx. This is the simplest form of streaming SSR in Next.js: the layout frame and navigation appear immediately while the actual page is still loading, without any manual Suspense import in the page itself.

For more fine-grained streaming SSR within a route, the automatic loading.tsx boundary is not enough, because it treats the entire page as one block. As soon as individual areas of a page should stream independently, say a fast page header and a slow data table, explicit <Suspense> components inside the Server Component itself are needed, as shown in the previous code example. loading.tsx and manual Suspense boundaries are not mutually exclusive, they complement each other: the route level for the coarse case, individual Suspense boundaries for fine control.


// app/products/[id]/loading.tsx — route-level streaming fallback
export default function Loading() {
  return (
    <div className="animate-pulse space-y-4 p-6">
      <div className="h-8 bg-slate-200 rounded w-1/3" />
      <div className="h-64 bg-slate-200 rounded" />
      <div className="h-4 bg-slate-200 rounded w-2/3" />
    </div>
  );
}

4. Selective Hydration: interactivity before full load

Streaming alone only delivers HTML, it does not yet make the page interactive. Selective Hydration is the mechanism that continues streaming SSR on the client: React hydrates already delivered chunks as soon as their JavaScript is loaded, regardless of whether other Suspense boundaries are still waiting on server data. That means a user can already interact with the header while content further down the page is still being loaded.

Selective Hydration becomes especially effective in combination with React 19's prioritization of interactions: if a user clicks an element whose hydration is not yet complete, React prioritizes that hydration over other, still invisible areas. This prioritization happens automatically, and it is one of the main reasons streaming SSR together with React 19 feels noticeably more responsive than older SSR implementations, where hydration always ran as a single, blocking step.

A side effect teams often overlook: because individual areas hydrate independently, every Suspense boundary must be functional on its own, without silently depending on a sibling's hydration. Global state shared via Context still works, but assumptions about a guaranteed hydration order between independent Suspense boundaries are no longer valid with streaming SSR.

5. Streaming SSR with renderToPipeableStream in detail

Under the hood, Next.js uses the function renderToPipeableStream from react-dom/server for Node.js environments, the actual low-level API behind streaming SSR. This function returns an object with a pipe method that writes the React tree directly into a Node response stream, chunk by chunk, as Suspense boundaries resolve. Anyone running their own SSR infrastructure without Next.js, for example in a custom Express server, works directly with this API.

Two callbacks are decisive for streaming SSR with renderToPipeableStream: onShellReady fires as soon as the initial HTML scaffold, the so-called shell, is ready and can be streamed to the client, even before all Suspense boundaries have resolved. onAllReady, on the other hand, waits until the entire tree is truly finished, which can matter for search engine crawlers that do not execute JavaScript or for static exports, where a complete document is needed instead of a stream.


// server.js — custom Node server using renderToPipeableStream directly
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

function handleRequest(req, res) {
  const { pipe, abort } = renderToPipeableStream(<App url={req.url} />, {
    bootstrapScripts: ['/client.js'],
    onShellReady() {
      // Shell is ready — start streaming immediately
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    },
    onShellError(error) {
      // Shell itself failed — fall back to a static error page
      res.statusCode = 500;
      res.send('<h1>Something went wrong</h1>');
    },
    onError(error) {
      console.error('Streaming error:', error);
    },
  });

  // Abort streaming after 10s to avoid hanging connections
  setTimeout(() => abort(), 10000);
}

6. Error handling: error.tsx and streaming boundaries

Error handling with streaming SSR is more complex than with classic SSR, because an error can occur after chunks have already been sent to the client and the HTTP status code 200 has long been sent. Next.js solves this with the error.tsx convention: if a component inside a Suspense boundary fails, Next.js automatically renders the nearest error.tsx as a replacement for exactly that chunk, without affecting parts of the page that were already delivered.

This behavior is a direct advantage of streaming SSR over traditional SSR: an error in a single component, say a broken third-party widget, does not take down the whole page, only the affected Suspense area. It is important to declare error.tsx as a Client Component, because React error boundaries, on which this convention is built, only work as Client Components, a common stumbling block for teams new to the App Router.

7. Measuring streaming SSR and Time to First Byte

Time to First Byte, TTFB for short, measures the time until the first byte of the response is received, and it is the metric most directly influenced by streaming SSR. With classic SSR, TTFB practically equals the server's total render time, because nothing is sent before everything is finished. With streaming SSR, TTFB drops drastically, because the shell is streamed as soon as it is ready, while individual Suspense content follows later.

Important for correct interpretation: TTFB alone says less with streaming SSR than with classic SSR, because a fast shell does not automatically mean the page is fully usable for the user. It helps to additionally measure Largest Contentful Paint and Time to Interactive, ideally with real user data through the Chrome User Experience Report program, instead of relying exclusively on synthetic lab measurements that do not reflect the actual network latency to the user.

8. Limits of streaming SSR: when it does not help

Streaming SSR improves perceived load time but changes nothing about the actual server load or the total amount of data that must be transported. An application whose database queries are fundamentally too slow only benefits from streaming to a limited degree: the shell appears quickly, but the user still waits for the actually relevant content, just with a skeleton instead of a blank screen.

For search engine crawlers that do not execute JavaScript and do not wait for chunks delivered later, streaming SSR can be problematic if important content ends up in a late Suspense boundary. React does ultimately deliver the complete tree for bots too, and modern crawlers like Googlebot now wait for completed rendering, but that is not a given for time-critical or resource-limited crawlers. Critical SEO-relevant content should therefore live in the shell itself whenever possible, not in a late-resolving Suspense boundary.

9. Streaming SSR compared

To properly place streaming SSR, a direct comparison with classic SSR, pure client-side rendering, and static generation helps.

Rendering model TTFB Blocks on slow data Suited for
Classic SSR High with slow queries Yes, the whole page Simple, fast data sources
Streaming SSR Low, shell immediately No, only individual Suspense areas Mixed fast/slow data sources
Client-side rendering Very low No, but blank screen first Highly interactive dashboards without SEO focus
Static generation Minimal, CDN delivery No, pre-built Rarely changing, cacheable content

The table shows that streaming SSR sits between classic SSR and client-side rendering: it keeps the SEO and security advantages of server-side rendering, but noticeably improves perceived load time compared to the all-or-nothing delivery of the classic model.

Mironsoft

React performance, SSR architecture, and Core Web Vitals

Pages that show something instantly instead of staying blank?

We build streaming SSR with cleanly cut Suspense boundaries, robust error handling, and measurable TTFB improvement, without losing SEO-relevant content in late chunks.

Suspense architecture

Cutting boundaries so fast content becomes visible immediately

Performance measurement

Evaluating TTFB, LCP, and Time to Interactive with real user data

Error isolation

Setting error.tsx boundaries so failures stay isolated

10. Summary

Streaming SSR solves the all-or-nothing problem of classic server-side rendering by having React Suspense segment the tree into independently resolving chunks. The shell appears immediately, slower areas follow once their data is ready, without fast content having to wait for slow content. Next.js makes this technique accessible with loading.tsx at the route level and allows finer control through manual Suspense boundaries in Server Components.

Selective Hydration continues this principle on the client, hydrating interactive areas independently of each other, prioritized by actual user interaction. The biggest limits of streaming SSR are not in the technology itself but in the fact that it does not reduce the underlying data load, and SEO-critical content must be placed in an early Suspense boundary to be reliably indexed.

Streaming SSR: The Essentials at a Glance

Suspense boundaries

Segment the render tree into independently resolving chunks, streamed in completion order.

loading.tsx

Automatic route-wide Suspense boundary in Next.js, complemented by manual Suspense components for fine control.

Selective Hydration

Hydrates independent areas individually, prioritized by real user interaction instead of rigid order.

Know the limits

Does not reduce data load. SEO-critical content belongs in the shell, not in a late-resolving boundary.

11. FAQ: Streaming SSR

1Difference from classic SSR?
Classic SSR delivers only after full rendering, streaming SSR sends chunks as soon as they are ready.
2Why does it need Suspense?
Suspense marks areas allowed to pause, providing the boundaries for chunk splitting.
3What does loading.tsx do?
Wraps the page automatically in a Suspense boundary, shows its content as fallback while loading.
4What is Selective Hydration?
Independent hydration of individual areas, prioritized by actual user interaction.
5What is renderToPipeableStream for?
Low-level React API for Node, writes the tree directly into a response stream. Next.js uses it internally.
6Error handling while streaming?
error.tsx replaces only the affected chunk, already delivered parts of the page stay untouched.
7Always better TTFB?
Yes, but view TTFB together with LCP and Time to Interactive, not in isolation.
8Good for SEO?
Generally yes, still place critical content in the shell, not in later-resolving boundaries.
9Does it reduce server load?
No, only improves perceived load time, not the actual compute load.
10Manual configuration needed?
Often loading.tsx or a Suspense component is enough, add manual boundaries for finer control.