Streaming SSR sends a page as a stream of HTML instead of one finished string. The server renders the parts outside every Suspense boundary first, sends that shell immediately, then streams the rest as each boundary resolves. The user sees a layout and placeholders right away instead of a blank screen, and the same stream powers selective hydration later.
Frameworks such as Next.js wire streaming automatically. The low-level API is renderToPipeableStream for Node.js and renderToReadableStream for edge runtimes with Web Streams.
The shell and the boundaries
The shell is everything outside Suspense boundaries. It is the earliest state the user can see, so it should look like a complete page skeleton rather than one giant spinner. Keep the shell minimal but complete, so the first paint is useful instead of a lone loading ring.
A root-level Suspense boundary would reduce the shell to a single spinner, which is why boundaries usually sit lower in the tree. Without streaming, the user waits for the slowest query before any HTML arrives at all.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}The layout and cover render into the shell immediately, while Posts waits behind its glimmer. When Posts finishes, React sends its HTML plus an inline script that swaps out the glimmer.
What the stream looks like
On the server, renderToPipeableStream emits the shell in onShellReady, then continues streaming. The client hydrates the same document afterward.
import { renderToPipeableStream } from "react-dom/server";
app.use("/", (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ["/main.js"],
onShellReady() {
response.setHeader("content-type", "text/html");
pipe(response);
},
});
});The shell is streamed the moment it is ready. Each Suspense boundary that resolves later is appended to the stream with the HTML that replaces its fallback, plus an inline script that performs the swap in place. For the boundary mechanics, see React Suspense Explained.
Because the response is a stream, the browser can paint partial HTML before the request finishes. That earlier first paint is the whole reason streaming SSR feels faster than a single render pass.
Nested boundaries create a sequence
Nesting Suspense boundaries splits the reveal into steps, so fast content appears before slow content.
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}The cover streams first, the sidebar when its data is ready, and the posts after that. Each boundary is one reveal point, so you control the visual order of arrival. Ordering the boundaries turns one slow request into a series of reveals, which feels faster than waiting for the whole page.
The tradeoff is more moving parts: every boundary adds a fallback and a swap, so place them at natural loading granularity instead of around every component.
Selective hydration
Streaming SSR does not wait for the JavaScript bundle to load, and hydration also happens in chunks. Suspense boundaries divide the tree into units that become interactive independently, so a fast section responds before a slow section finishes. A search box can stay clickable while a heavy comments section below it is still streaming.
Hydration order follows the same boundaries, so interactivity arrives with the content that matters first. A section that never suspends hydrates immediately, while a suspended section waits for its data.
A Promise read with use is what makes a boundary suspend. See The React use API for the data side, and React Server Components Explained for how the server side renders the same tree.
Crawlers, errors, and timeouts
For crawlers and static generation, onAllReady waits for everything and sends the final HTML instead of a progressive stream. Errors inside the shell call onShellError, while errors outside it recover by streaming the fallback and retrying on the client.
You can also abort a slow render after a timeout, which flushes the remaining fallbacks and finishes the work on the client. Frameworks expose these same controls through their configuration, but the mechanics are identical.
The abort path trades completeness for speed, which is useful when a third-party request hangs. The user still gets the shell, and the slow part finishes on the client.
Rune AI
Key Insights
- Streaming sends the shell first, then the rest as data resolves.
- Suspense boundaries decide what loads in the shell.
- Nested boundaries create a granular reveal sequence.
- Selective hydration makes parts interactive in chunks.
- Frameworks automate this on top of renderToPipeableStream.
Frequently Asked Questions
Which API streams HTML in Node.js?
Does Suspense detect data fetched in an Effect?
Conclusion
Streaming SSR sends the shell first and the rest of the page as Suspense boundaries resolve. Place boundaries around the slow parts so the layout arrives immediately and content reveals in order.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.