React Server Components vs Islands Architecture

Photo by Capt. Anastasia Schmidt via Wikimedia Commons (Public domain)
React Server Components default every component to the server and require an explicit use client directive to ship interactive JavaScript, so hydration happens only at those boundaries. Islands architecture, used by Astro and Qwik, defaults the whole page to static HTML and hydrates only components you explicitly mark as islands. RSC asks you to justify client code; islands ask you to justify interactivity.
No. Server components themselves ship no JavaScript and have zero hydration cost, but any component marked with use client still hydrates in the browser like normal React. RSC reduces hydration to the interactive boundaries of your tree rather than removing it entirely. Qwik's resumability is the model that actually avoids hydration replay altogether.
Partial hydration, as in Astro, still hydrates each interactive island by re-executing its component code in the browser. Qwik's resumability instead serialises application state and event handler locations into the HTML and resumes on the client without re-running that code. JavaScript for a handler is fetched only when the user actually interacts, keeping upfront execution near zero.
Choose Astro when your site is content-first — marketing pages, documentation, blogs, or landing pages where most content is static and interactivity is the exception. Astro ships zero JavaScript by default and lets you add small isolated islands. Next.js with RSC fits better for app-shaped products with dense interactivity and shared client state.
Usually, but only if you keep the use client boundary near the leaves of your component tree. If you mark a top-level layout as a client component, every child gets pulled into the client bundle and the savings disappear. Push the directive as far down toward the actual interactive element as possible to maximise the reduction.

Photo by Capt. Anastasia Schmidt via Wikimedia Commons (Public domain)
Key Takeaway
React Server Components render on the server and ship zero JavaScript unless a component is marked interactive, so hydration cost drops to the interactive boundaries only. Islands architecture in Astro and Qwik inverts the default: the page is static HTML, and you opt specific components into hydration or resumability. Both cut JavaScript; they differ in where the boundary lives.
Every modern frontend framework is now chasing the same goal: send the browser less JavaScript. The single-page-app era taught us the cost of shipping a full React runtime plus every component to the client, then re-running it all during hydration just to attach event listeners to HTML the server already produced. Two families of answers have emerged. React Server Components (RSC), now stable in React 19 and the default in Next.js 15, and islands architecture, popularised by Astro and pushed further by Qwik's resumability model.
I have shipped both. On a Next.js service I run, moving data-heavy pages to server components cut the client bundle noticeably because the fetching, formatting, and markup never crossed to the browser. On a mostly-static marketing site I rebuilt in Astro, the same instinct applied differently: the page started as HTML and I only paid for JavaScript where I explicitly asked for it. Understanding why those two experiences felt different starts with the hydration model.
Classic server-side rendering sends fully-formed HTML, which is great for first paint. But the browser then downloads the JavaScript bundle, rebuilds the component tree in memory, walks the existing DOM, and attaches listeners. That replay is hydration, and its cost scales with the size of your interactive tree, not with how much of the page is genuinely interactive. A page that is ninety percent static text still pays to hydrate all of it if it is one big React tree. That is the waste both approaches attack.
In the RSC model every component is a server component by default. It runs once on the server, can await data directly, and emits a serialised description of the UI that the client renders without ever receiving the component's own JavaScript. You cross into client territory only when you add the use client directive at the top of a file. Everything from that boundary down ships to the browser and hydrates; everything above it does not. The mental shift is that interactivity is now something you opt into explicitly, file by file.
// app/dashboard/page.tsx — a Server Component (no directive)
// Runs on the server, awaits data, ships NO JS for this file.
export default async function Dashboard() {
const invoices = await db.invoice.findMany({ take: 20 });
return (
<section>
<h1>Invoices</h1>
<InvoiceTable rows={invoices} /> {/* static, server-rendered */}
<RefreshButton /> {/* client island, see below */}
</section>
);
}
// app/dashboard/refresh-button.tsx
'use client'; // <- the hydration boundary
import { useRouter } from 'next/navigation';
export function RefreshButton() {
const router = useRouter();
return <button onClick={() => router.refresh()}>Refresh</button>;
}Keep use client at the leaves of your tree, not the root. If you mark a top-level layout as a client component, every child gets pulled into the client bundle even if it never needed to. Push the directive as far down toward the actual interactive element as you can.
Astro inverts the default in the other direction. A page is static HTML with no client JavaScript at all until you hydrate a specific component, an island, with a client directive. The directive also controls when hydration happens: client:load fires immediately, client:idle waits for the main thread to settle, client:visible waits until the component scrolls into the viewport, and client:media waits for a CSS media query. Each island is isolated; they do not share a runtime and cannot directly talk to each other, which is the intended trade-off for keeping them small and independent.
---
// index.astro — everything here is static HTML, zero JS
import Hero from '../components/Hero.astro';
import PriceCalculator from '../components/PriceCalculator.jsx';
import Newsletter from '../components/Newsletter.jsx';
---
<Hero />
<!-- Hydrate only when it scrolls into view -->
<PriceCalculator client:visible />
<!-- Hydrate after the browser is idle -->
<Newsletter client:idle />Qwik takes the islands idea to its logical extreme with resumability. Instead of hydrating at all, Qwik serialises the application state and the location of every event handler into the HTML, then resumes on the client without re-executing the component code the server already ran. JavaScript for a handler is fetched only when the user actually triggers it. The result is that initial JavaScript execution stays near zero regardless of how large the app is, because there is no hydration replay to pay for up front.
| Dimension | React Server Components | Islands (Astro / Qwik) |
|---|---|---|
| Default rendering | Server component; opt into client with use client | Static HTML; opt into interactivity per island |
| Hydration model | Partial — only client boundaries hydrate | Astro: partial per island. Qwik: none (resumability) |
| Client JS baseline | React runtime plus interactive components | Astro: per-island runtime. Qwik: near-zero upfront |
| Data fetching | Directly in the server component with await | In frontmatter / loaders, then passed to islands |
| Cross-component state | Shared React tree; context works across client parts | Islands are isolated; sharing needs stores or events |
| Best fit | App-like, data-driven, heavily interactive products | Content sites, marketing, docs, blogs with sprinkles of UI |
Do not pick an architecture on benchmarks alone. Qwik's resumability trades hydration cost for state serialisation and lazy fetching, which adds its own complexity and edge cases. RSC pushes work to the server, so your server capacity and data-layer latency become the new bottleneck. Measure the trade-off against your actual traffic and team, not a demo.
There is no universal winner here, and that is the honest conclusion. RSC and islands are two answers to the same problem, phrased from opposite defaults: RSC assumes server-rendered and asks you to justify client code, while islands assume static and ask you to justify interactivity. Pick the default that matches the shape of what you are building, and you will fight your framework far less.