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.
What hydration actually costs
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.
React Server Components: the boundary is a directive
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.
Islands: the page is static, interactivity is an exception
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.
The comparison that matters
| 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 |
How I actually choose
- Reach for RSC when the product is app-shaped — dashboards, authenticated flows, dense interactivity, shared client state across widgets. The full React ecosystem and a single mental model are worth the baseline runtime.
- Reach for Astro when the site is content-first — marketing pages, docs, blogs, landing pages — where most of the page is genuinely static and interactivity is the exception, not the rule.
- Reach for Qwik when you need app-level interactivity but cannot tolerate the upfront hydration cost — very large surfaces where time-to-interactive must stay flat as the app grows.
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.

