Streaming SSR, Islands, and the New Rendering Paradigm
How React Server Components, Suspense streaming, and islands architecture replaced monolithic hydration — and how to compose static shells, streamed regions, and interactive islands in one page.
Published on • August 11, 2026
AI Assistant

Every senior dev has shipped the SPA that “feels” fast on a perfect laptop and dies on a mid-range phone in a poor network: a 300KB+ main bundle, a blank screen until JavaScript parses, and a hydration pass that re-executes on the client the exact same DOM the server already produced. The old binary choice — render everything server-side, or render everything client-side — is dead. The new paradigm is per-region composition: a static shell streams from the server, dynamic regions stream as their data resolves, and only the genuinely interactive widgets ship JavaScript at all.
In this post, you will learn how React Server Components (RSC), streaming SSR with Suspense, and the islands architecture compose into that model, how to choose between them, and how to write a production page that mixes all three. Key technologies: React 19 (RSC stable since December 2024), Astro 5, Next.js App Router, and the Flight wire protocol.
Prerequisites
- Working knowledge of React and JSX (components, props, hooks).
- Familiarity with what SSR and hydration are and why hydration has a cost.
- Node.js 20+ installed to run the examples.
- A browser with DevTools Network panel open to watch streams arrive.
The problem with monolithic hydration
Classic SSR solves the blank screen, then reintroduces it differently. The server renders HTML, the browser paints it — and then downloads, parses, and executes the entire application bundle so that event handlers can attach. Until that hydration pass finishes, the page is a picture, not an app. The cost grows linearly with the app: a SaaS dashboard hydrates a comments panel, a chart, a settings form, and an analytics grid all at once, even when the user only ever clicks the first one.
Two architectures attack the same waste — shipping JavaScript for parts of a page that will never respond to a click — and they take different routes:
- Streaming SSR decouples the time-to-first-byte (TTFB) from the slowest data dependency. The server flushes the static shell immediately and streams each Suspense boundary as its data resolves.
- Islands architecture makes static HTML the default and lets each interactive component opt in to client JavaScript. A fully static page ships zero framework runtime.
The two are orthogonal and complementary: streaming controls when HTML arrives; islands control what JavaScript executes.
Streaming SSR with Suspense
React 18 introduced streaming via renderToPipeableStream on Node and renderToReadableStream for web streams. Instead of serializing the whole tree to one string, the server flushes the shell and lets each Suspense boundary stream in independently:
import { Suspense } from 'react';
import { renderToReadableStream } from 'react-dom/server';
async function Handler(request: Request) {
const stream = await renderToReadableStream(
<html>
<body>
<Header /> {/* ships immediately */}
<Suspense fallback={<FeedSkeleton />}>
<Timeline /> {/* slow DB query */}
</Suspense>
<Suspense fallback={<RecommendedSkeleton />}>
<Recommended /> {/* slow external API */}
</Suspense>
</body>
</html>,
{ onError: console.error }
);
return new Response(stream, {
headers: { 'Content-Type': 'text/html' },
});
}
The browser receives <Header> and both skeletons as the first chunk — TTFB is near-zero. As the timeline query and the recommendations API resolve, the server flushes each boundary’s HTML inline, replacing the fallback. Streaming works over Transfer-Encoding: chunked, so the browser’s parser builds the DOM progressively with zero client JavaScript.
Two details matter in production:
- Keep boundaries parallel. In React 19, siblings inside one Suspense boundary render sequentially after one suspends. Split independent fetches into separate boundaries so they stream in parallel.
- Boundary granularity controls the stream. Too coarse and one slow query blocks everything; too fine and you fragment the response into many round-trips.
React Server Components: fewer bytes, not just earlier bytes
Streaming makes the HTML arrive faster, but the bundle problem remains. React Server Components (RSC) attack it directly: components that run only on the server contribute zero bytes to the client bundle. RSC was stable as of React 19 and works through a serialized, streamable tree format called Flight — not HTML, not JSON, but a row-based stream the client runtime reconciles.
// app/article/[id]/page.tsx — a Server Component (no "use client")
import { db } from '@/lib/db';
import { Reactions } from './Reactions';
export default async function ArticlePage({ params }: { params: { id: string } }) {
const article = await db.articles.findOne({ id: params.id });
return (
<main>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.html }} />
<Reactions articleId={article.id} /> {/* "use client" island */}
</main>
);
}
The Reactions module carries a "use client" directive, so it ships to the browser and hydrates; the surrounding Server Component queries the database directly, renders on the server, and never crosses the wire as code. The key constraint is directional: server components can render client components, but a client component cannot import a server component — it can only receive one as children.
The tradeoff that matters architecturally: RSC has a non-zero bundle floor (the React runtime plus the Flight decoder always load), but adding more Server Components adds Flight bytes, not bundle bytes. Islands, by contrast, have a ~0KB floor but pay per island, and they require manual cross-boundary communication.
Islands architecture: static by default
Astro is the reference implementation of islands. The page is static, crawlable HTML by default; every interactive component is an island that opts in via a client directive. An Astro page with one React island ships React only on the route that includes it — and zero JavaScript on fully static routes.
---
import Header from '../components/Header.astro';
import Cart from '../components/Cart.tsx';
import SearchBox from '../components/SearchBox.svelte';
---
<html>
<head><title>Shop</title></head>
<body>
<Header />
<main>
<p>Product catalog — pure static HTML, zero JS shipped here.</p>
<SearchBox client:visible /> {/* hydrate when scrolled into view */}
<Cart client:idle /> {/* hydrate when the browser is idle */}
</main>
</body>
</html>
Each island carries its own runtime and hydrates independently — a React island and a Svelte island can coexist on one page. The directives tune when each island hydrates: client:load immediately, client:visible on viewport intersection, client:idle after the main thread goes quiet, client:media on a media query. Over-marking every component client:load collapses islands back into a monolithic hydration pass — the anti-pattern to avoid.
Cross-island state is the hard part. The reliable pattern is a neutral transport layer: each island publishes state to a CustomEvent or BroadcastChannel, and subscribers react without sharing a store reference across runtimes.
Choosing per region, not per app
The 2026 decision is not “pick CSR or SSR” — it is, for each region of each page, where it renders, what ships, and when it hydrates:
| Architecture | Bundle floor | Interactivity | Best for |
|---|---|---|---|
| Islands (Astro 5) | ~0 KB static | Per-island, any framework | Content sites, docs, marketing |
| RSC + SSR (Next.js App Router) | React runtime + Flight decoder | Selective within one tree | React apps mixing static + dynamic |
| Streaming SSR | Your app (no island split) | After hydration | Data-heavy pages dominated by one slow fetch |
| Resumability (Qwik) | ~1 KB Qwikloader | Event-driven chunk fetch | Interaction-latency-bound mobile apps |
Reach for islands when the content is primary and the framework runtime is the largest byte on the page. Reach for RSC when you have a React tree with heavy server-only dependencies (markdown rendering, large schemas, SDKs) and a client interactive surface you can keep small. Reach for plain streaming SSR when the page is interactive everywhere and one slow dependency is dominating TTFB. And watch the crossover point: as interactivity climbs and interleaves with server data, RSC’s unified tree and gentle slope overtake a proliferation of islands.
Putting It All Together
A runnable end-to-end version of the streaming shell + Suspense + client island combo is available here: https://gist.github.com/redlinesoft/streaming-ssr-islands-example
Expected output when you run the gist and open the page:
$ node server.js
Streaming SSR listening on http://localhost:3000
[server] flushed shell: header + skeletons (TTFB ~2ms)
[server] resolved Timeline boundary after 400ms -> streamed
[server] resolved Recommended boundary after 900ms -> streamed
[client] hydrated <Reactions/> island only (3.1 kB JS, no React on static shell)
[client] network waterfall: HTML chunks arrive at 2ms / 402ms / 902ms; JS loaded once, at idle
The Network panel shows the key evidence: three HTML chunks arriving progressively, one tiny island chunk, and zero JavaScript for the static regions of the page.
Conclusion & Next Steps
You now understand the three layers of the new rendering paradigm: streaming SSR decouples TTFB from slow data, RSC keeps server-only components off the client bundle entirely, and islands make static HTML the default with interactive widgets opting in. Next steps: build a content page with one client:visible island in Astro and measure the bundle delta; convert one slow dashboard route to streaming SSR with separate Suspense boundaries; then read the React documentation on Server Components and experiment with a "use client" boundary in a Next.js App Router project.
References / Sources
- React documentation — Server Components, Suspense, and streaming SSR. https://react.dev
- MDN — Streaming HTML in the browser. https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
- Islands Architecture — the original essay by Jason Miller and the framework-specific guide to streaming SSR. https://www.islands-architecture.com/
- Astro docs — client directives and server islands. https://docs.astro.build/en/concepts/islands/