React Server Components vs Astro Islands: Partial Hydration 2026
React Server Components and Astro Islands both solve the same problem — shipping too much JavaScript — but they arrive at different solutions with completely different developer experiences. In production, the choice between them can mean a 3x difference in JavaScript payload for content-heavy sites.
TL;DR
Astro Islands wins for content-heavy sites where most pages have minimal interactivity — blogs, documentation, marketing. React Server Components wins for application-heavy sites where users interact deeply with the UI and navigate frequently — dashboards, SaaS apps, e-commerce with complex flows. The key differentiator is your interactivity-to-content ratio.
Key Takeaways
- Astro default: 0 kB JavaScript; React default: ~46 kB (React + ReactDOM) plus component code
- Astro Islands uses client directives (
client:load,client:idle,client:visible) for selective hydration - RSC ships React runtime plus all client components as a bundle; Islands ship per-island code only
nextnpm weekly downloads: ~7M;astro: ~2M- RSC pages navigate instantly after initial load (client-side routing); Astro pages do full page navigation (unless using
<ViewTransitions>) - RSC enables data fetching in components without client-side waterfalls
- Astro 5 supports islands from React, Svelte, Vue, Solid, or Preact in the same file
The Core Problem: Too Much JavaScript
Modern web pages ship more JavaScript than they need. A typical React app includes:
- The React runtime (~46 kB)
- All component code, including for components not visible on the current page
- State management, routing, data fetching libraries
For a blog post that has one interactive comment form and is otherwise static content, this is waste. The question is: how do you ship JavaScript only where needed?
Astro Islands: Static-First with Explicit Islands
Astro's approach is static by default, interactive where specified.
A normal Astro component outputs zero JavaScript:
---
// Counter.astro — this generates static HTML only
const { initialCount = 0 } = Astro.props;
---
<div>Initial count: {initialCount}</div>
To make a component interactive (an "island"), you use a framework component with a client: directive:
---
// Page.astro
import StaticHeader from './StaticHeader.astro';
import Counter from './Counter.jsx'; // React component
import Comments from './Comments.svelte'; // Svelte component
---
<html>
<body>
<!-- Static — zero JavaScript -->
<StaticHeader title="My Blog Post" />
<!-- Article content — static HTML -->
<article>...</article>
<!-- React island — loads when visible -->
<Counter client:visible initialCount={0} />
<!-- Svelte island — loads when browser is idle -->
<Comments client:idle postId="123" />
</body>
</html>
Hydration Strategies
| Directive | When JavaScript Loads |
|---|---|
client:load | Immediately on page load |
client:idle | When browser is idle (requestIdleCallback) |
client:visible | When component scrolls into viewport |
client:media="(max-width: 768px)" | When media query matches |
client:only="react" | Client-only, never SSR |
This is surgical: a page with one client:visible island loads essentially no JavaScript until the user scrolls to that component.
JavaScript Payload Comparison
For a documentation site with 1 interactive search widget:
| Approach | Initial JS Payload |
|---|---|
| Astro (search island only) | 15-25 kB (just search component) |
| Create React App | 180-250 kB |
| Next.js (App Router) | 90-150 kB |
| Next.js (App Router, RSC) | 50-90 kB |
Multi-Framework Islands
Astro 5 supports mixing frameworks in one page:
---
import ReactChart from './Chart.jsx'; // React
import SvelteCarousel from './Carousel.svelte'; // Svelte
import VueForm from './ContactForm.vue'; // Vue
---
<ReactChart client:visible data={chartData} />
<SvelteCarousel client:idle images={photos} />
<VueForm client:load />
Each island is isolated — they don't share React context or Vue's reactivity. Communication between islands requires custom events or shared state in a non-framework layer (like a Nano Stores store).
React Server Components: Server-First React
RSC's approach is different: it keeps React as the programming model but moves components to the server when possible.
The RSC Mental Model
// This component runs ONLY on the server
// It can access databases, file systems, secrets
// Its code is NEVER sent to the browser
async function UserProfile({ userId }: { userId: string }) {
const user = await db.user.findUnique({ where: { id: userId } });
const posts = await db.post.findMany({ where: { authorId: userId } });
return (
<div>
<h1>{user.name}</h1>
<p>Member since {user.createdAt.toLocaleDateString()}</p>
{/* Client Component for interactivity */}
<FollowButton userId={userId} initialFollowing={user.isFollowed} />
<PostList posts={posts} />
</div>
);
}
// This component runs on BOTH server and client
// The code IS sent to the browser
'use client';
import { useState } from 'react';
function FollowButton({ userId, initialFollowing }: Props) {
const [following, setFollowing] = useState(initialFollowing);
return (
<button onClick={() => toggleFollow(userId, setFollowing)}>
{following ? 'Following' : 'Follow'}
</button>
);
}
The key: UserProfile never ships to the browser. Only FollowButton does.
Data Fetching Without Waterfalls
One of RSC's biggest DX wins: you can fetch data in components without client-side waterfalls:
// OLD WAY (Client Components): waterfall
function Page() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
useEffect(() => {
// Fetch 1: get user
fetchUser().then(u => {
setUser(u);
// Fetch 2: get posts (waits for fetch 1)
fetchPosts(u.id).then(setPosts);
});
}, []);
}
// NEW WAY (Server Components): parallel, no waterfall
async function Page({ params }: { params: { userId: string } }) {
// These run in parallel on the server
const [user, posts] = await Promise.all([
db.user.findUnique({ where: { id: params.userId } }),
db.post.findMany({ where: { authorId: params.userId } }),
]);
return <UserProfile user={user} posts={posts} />;
}
No loading states, no waterfalls, no exposed database credentials in client code.
Streaming with RSC
Next.js with RSC supports streaming via <Suspense>:
import { Suspense } from 'react';
export default async function Dashboard() {
return (
<div>
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart /> {/* Slow async component */}
</Suspense>
<Suspense fallback={<LatestInvoicesSkeleton />}>
<LatestInvoices /> {/* Another slow component */}
</Suspense>
</div>
);
}
The page streams HTML — the shell loads instantly, slow components resolve progressively.
Side-by-Side Architecture Comparison
| Aspect | Astro Islands | React Server Components |
|---|---|---|
| Default JS payload | 0 kB | ~46 kB + components |
| Hydration model | Per-island, explicit | Whole page (client components) |
| Server data fetching | Astro.props / Astro.fetch | async component functions |
| Client navigation | Full page (with View Transitions) | SPA-style (React router) |
| Multi-framework | Yes (React, Svelte, Vue) | React only |
| Shared state between islands | Complex | Natural (React context) |
| Learning curve | Low | Medium |
| Best for | Content sites | Applications |
Performance in Production
Content Site (Documentation)
| Metric | Astro Islands | Next.js RSC |
|---|---|---|
| Lighthouse Performance | 98-100 | 85-95 |
| First Contentful Paint | 0.4-0.8s | 0.8-1.5s |
| Time to Interactive | 0.5-1.0s | 1.2-2.5s |
| JS payload (initial) | 0-30 kB | 60-150 kB |
Application (SaaS Dashboard)
| Metric | Astro Islands | Next.js RSC |
|---|---|---|
| Client navigation speed | Full reload (~800ms) | Instant (<100ms) |
| Shared state complexity | High | Low |
| Real-time features | Manual | React Query / SWR |
| Nested layouts | Manual | Native (App Router) |
The Navigation Problem
Astro's biggest weakness for applications: navigation is a full page load by default. Astro 5 added View Transitions which provides smooth animations but still does full page reloads. Next.js with RSC does client-side navigation for the full React subtree — after the first load, navigating between pages is instant.
For applications where users click between pages hundreds of times per session, this matters enormously.
The Isolation Problem
Astro's biggest structural challenge: islands are isolated. You can't easily share React context between a <Cart> island in the header and a <ProductDetail> island in the page body. You need a framework-agnostic state solution (Nano Stores, a custom event bus, or localStorage). RSC doesn't have this problem — client components in the same React tree share context naturally.
npm Package Considerations
For Astro:
npm install astro
# Each framework integration:
npm install @astrojs/react @astrojs/svelte @astrojs/vue
For Next.js with RSC:
npm install next react react-dom
| Package | Weekly Downloads |
|---|---|
next | ~7M |
astro | ~2M |
@astrojs/react | ~600K |
Choose Astro Islands if:
- Building a blog, documentation site, or marketing page
- Most pages are static with a few interactive elements
- You want maximum Lighthouse scores out of the box
- You want to mix React, Svelte, and Vue in one project
- Performance is a top priority and interactivity is limited
Choose RSC (Next.js App Router) if:
- Building a web application where users navigate repeatedly
- Complex shared state across many components
- Data fetching in components is important to you
- You want client-side navigation speed
- Your team knows React and doesn't want to learn Astro
- You need features like React Context across your UI
The Hybrid Architecture
For complex projects, many teams use both:
- Astro for marketing/docs site (fast, SEO-friendly, low JS)
- Next.js for the application (SaaS dashboard, user portal)
These are separate deployments — there's no technical way to mix RSC and Astro Islands in the same application boundary.
Explore on PkgPulse
Compare download trends for next vs astro on PkgPulse.
See the live comparison
View rsc vs. astro islands on PkgPulse →