React Server Components vs Astro Islands: Partial Hydration in 2026
·PkgPulse Team
TL;DR
Both RSC and Astro Islands solve the same problem — "don't ship JavaScript for static content" — but from opposite directions. RSC starts from React and marks some components as server-only. Astro starts from zero-JS and you opt-in to interactive islands. For apps that are mostly interactive (SaaS dashboards, complex UIs), RSC is better. For sites that are mostly static with some interactivity (blogs, marketing, docs), Astro Islands wins on bundle size and simplicity.
Key Takeaways
- RSC: React-native, streaming, server/client component boundary, works in Next.js 15
- Astro Islands: any framework (React/Vue/Svelte), zero-JS default, explicit hydration directives
- npm downloads:
astroat ~850K/week growing fast;nextat ~9M/week dominant - Bundle size: Astro default = 0KB JS; RSC = React runtime (~46KB) + only interactive components
- Streaming: RSC has Suspense streaming; Astro has partial hydration (not streaming)
- Complexity: RSC requires understanding client/server boundary; Astro is more explicit
Download Trends
| Package | Weekly Downloads | Trend |
|---|---|---|
next | ~9M | → Stable |
astro | ~850K | ↑ +40% YoY |
react | ~25M | → Stable |
Astro is growing fast in the content/marketing site space.
React Server Components
The Client/Server Boundary
// app/dashboard/page.tsx — Server Component (default)
// Runs on server, never ships to client, can access DB directly:
import { db } from '@/lib/db';
import { auth } from '@/auth';
import { Suspense } from 'react';
import { MetricsChart } from './metrics-chart'; // Client Component
export default async function DashboardPage() {
const session = await auth();
// Direct DB access — no API needed:
const user = await db.user.findUnique({ where: { id: session!.user.id } });
return (
<div>
{/* Server-rendered HTML — no JS shipped for this: */}
<h1>Welcome, {user?.name}</h1>
<p>Member since {user?.createdAt.toLocaleDateString()}</p>
{/* Interactive component — ships React + component JS: */}
<Suspense fallback={<div className="animate-pulse h-64 bg-gray-100" />}>
<MetricsChart userId={user!.id} />
</Suspense>
</div>
);
}
// components/metrics-chart.tsx — Client Component
// 'use client' marks the client boundary:
'use client';
import { useEffect, useState } from 'react';
import { LineChart, Line, XAxis, YAxis } from 'recharts';
export function MetricsChart({ userId }: { userId: string }) {
const [data, setData] = useState<DataPoint[]>([]);
useEffect(() => {
fetch(`/api/metrics/${userId}`).then(r => r.json()).then(setData);
}, [userId]);
return (
<LineChart data={data}>
<XAxis dataKey="date" />
<YAxis />
<Line type="monotone" dataKey="value" stroke="#3b82f6" />
</LineChart>
);
}
Streaming with Suspense
// app/products/page.tsx — streams in components as they resolve:
import { Suspense } from 'react';
async function ProductList() {
const products = await db.product.findMany(); // Slow query
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
export default function ProductsPage() {
return (
<div>
<h1>Products</h1> {/* Renders immediately */}
<Suspense fallback={<ProductListSkeleton />}>
<ProductList /> {/* Streams in when DB query resolves */}
</Suspense>
</div>
);
}
Astro Islands
Zero-JS Default
---
// src/pages/blog/[slug].astro
// This entire file is server-rendered, zero JS by default:
import { getPost } from '../lib/content';
const { slug } = Astro.params;
const post = await getPost(slug);
---
<html>
<body>
<h1>{post.title}</h1>
<div set:html={post.html} />
<!-- No JS shipped — just HTML -->
</body>
</html>
Islands: Explicit Hydration
---
import Counter from '../components/Counter.tsx'; // React component
import CommentForm from '../components/CommentForm.svelte'; // Svelte!
---
<div>
<!-- Static HTML — no JS: -->
<h1>My Article</h1>
<!-- Island: hydrates on load, ships React runtime: -->
<Counter client:load />
<!-- Island: lazy-hydrates when visible in viewport: -->
<CommentForm client:visible />
<!-- Island: hydrates only on user interaction: -->
<ShareButton client:idle />
<!-- Island: never hydrates (just SSR): -->
<Timestamp client:only="react" />
</div>
Hydration Directives
| Directive | When Hydrates | Use Case |
|---|---|---|
client:load | Immediately | Critical above-fold interactivity |
client:idle | After page load | Non-critical UI |
client:visible | When visible | Below-fold content |
client:media | When CSS media matches | Mobile-only components |
client:only | SSR skip, hydrate client-only | Components with browser APIs |
Bundle Size Reality
Astro page with 0 islands:
→ ~0KB JavaScript
→ Fast LCP, perfect Lighthouse scores
Astro page with 2 React islands:
→ React runtime (~46KB) + island components (~5-10KB)
→ ~55KB total
Next.js page with RSC + 1 client component:
→ React runtime (~46KB) + client component (~2KB) + Next.js runtime (~30KB)
→ ~78KB total
Next.js full SPA page (no RSC optimization):
→ ~150-400KB depending on complexity
Recommendation
Use RSC (Next.js) if:
→ Building a complex interactive SaaS app
→ Need real-time data, user sessions, complex state
→ Want streaming data from server to client
→ Team is React-only
Use Astro Islands if:
→ Building a content site, blog, docs, marketing pages
→ SEO and Core Web Vitals are top priority
→ Want to mix React + Vue + Svelte components
→ Most content is static with occasional interactivity
Compare Astro and Next.js download trends on PkgPulse.