Skip to main content

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: astro at ~850K/week growing fast; next at ~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

PackageWeekly DownloadsTrend
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

DirectiveWhen HydratesUse Case
client:loadImmediatelyCritical above-fold interactivity
client:idleAfter page loadNon-critical UI
client:visibleWhen visibleBelow-fold content
client:mediaWhen CSS media matchesMobile-only components
client:onlySSR skip, hydrate client-onlyComponents 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.