Skip to main content

React Server Components vs Astro Islands: Partial Hydration 2026

·PkgPulse Team

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
  • next npm 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

DirectiveWhen JavaScript Loads
client:loadImmediately on page load
client:idleWhen browser is idle (requestIdleCallback)
client:visibleWhen 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:

ApproachInitial JS Payload
Astro (search island only)15-25 kB (just search component)
Create React App180-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

AspectAstro IslandsReact Server Components
Default JS payload0 kB~46 kB + components
Hydration modelPer-island, explicitWhole page (client components)
Server data fetchingAstro.props / Astro.fetchasync component functions
Client navigationFull page (with View Transitions)SPA-style (React router)
Multi-frameworkYes (React, Svelte, Vue)React only
Shared state between islandsComplexNatural (React context)
Learning curveLowMedium
Best forContent sitesApplications

Performance in Production

Content Site (Documentation)

MetricAstro IslandsNext.js RSC
Lighthouse Performance98-10085-95
First Contentful Paint0.4-0.8s0.8-1.5s
Time to Interactive0.5-1.0s1.2-2.5s
JS payload (initial)0-30 kB60-150 kB

Application (SaaS Dashboard)

MetricAstro IslandsNext.js RSC
Client navigation speedFull reload (~800ms)Instant (<100ms)
Shared state complexityHighLow
Real-time featuresManualReact Query / SWR
Nested layoutsManualNative (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
PackageWeekly 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.

Comments

Stay Updated

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