← Blog9 min read#astro#islands#partial hydration

Astro Islands Architecture: Partial Hydration in Practice

Astro's islands architecture ships zero JS by default and hydrates only what needs it - here's how partial hydration actually works and when to reach for each directive.

abstract colorful gradient light streaks representing modular architecture

What Islands Architecture Actually Means

Most React apps ship a single, monolithic JavaScript bundle. The browser downloads it, parses it, and runs it - all before the user can interact with anything. Astro took a different path. In Astro 1.0 (released August 2022), the team formalized something called islands architecture: the page is mostly static HTML, and interactive components are isolated "islands" that hydrate independently.

The mental model is straightforward. Picture a mostly static marketing page - header, hero text, a comparison table, a footer. One widget in the corner needs JavaScript: a live pricing calculator. With a traditional React SPA, you'd hydrate the *entire page* just to run that one widget. With islands, only the calculator island ever gets JavaScript. Everything else stays as pure, zero-cost HTML.

That said, this isn't a new idea. Jason Miller coined the term in 2020, and Eleventy, Marko, and Fresh all have their own takes on it. What Astro nailed was the developer experience - the client:* directive system makes it feel almost trivially easy to opt components into hydration on a case-by-case basis.

Worth noting: islands architecture doesn't mean you can't build interactive UIs. You absolutely can. It means you're deliberate about *where* JavaScript lives. The default is zero JS. You add it back surgically.

The client:* Directive System, Explained

This is where Astro earns its reputation. Every interactive component you use in an .astro file needs a client:* directive to hydrate. Without one, Astro renders it to static HTML at build time and ships no JavaScript for it at all. Five directives, five different hydration strategies - and picking the right one matters more than most tutorials admit.

client:load hydrates immediately on page load. It's the "default React" behavior. Use it only for components that are visible in the viewport on first paint and genuinely need interactivity right away - things like nav menus with dropdowns or modals triggered by above-the-fold buttons. Don't reach for it out of habit.

---
import PricingCalculator from '../components/PricingCalculator.jsx';
import NewsletterForm from '../components/NewsletterForm.jsx';
import LazyChart from '../components/LazyChart.jsx';
---

<!-- Hydrates immediately: visible + interactive on load -->
<PricingCalculator client:load />

<!-- Hydrates when the browser is idle -->
<NewsletterForm client:idle />

<!-- Hydrates only when scrolled into view -->
<LazyChart client:visible />

client:idle defers hydration until the browser's requestIdleCallback fires - after critical work is done. This is perfect for below-the-fold CTAs, chat widgets, or anything that doesn't need to be ready in the first 200ms. client:visible goes further: it uses an IntersectionObserver and won't hydrate until the element actually enters the viewport. A chart 1,200px below the fold? That's a client:visible candidate every time.

client:media is the underused gem - it hydrates only when a CSS media query matches. A mobile hamburger menu component? client:media="(max-width: 768px)". Desktop users download zero bytes of that component's JavaScript. One more thing - client:only skips server-side rendering entirely and hydrates purely client-side. You'd use it for components that genuinely can't render on the server: anything touching window, WebGL, or browser-only APIs.

Setting Up an Astro Project with React Islands

Astro supports React, Vue, Svelte, Solid, Preact, and Lit as island frameworks - simultaneously, in the same project. That's not a typo. You can have a React pricing widget and a Svelte animation component on the same page, and Astro will only ship the runtime for each framework to the islands that actually use it.

# Scaffold a new Astro project (Astro 4.x as of 2026)
npx create astro@latest my-site --template minimal
cd my-site

# Add the React integration
npx astro add react

# Optional: add Tailwind while you're at it
npx astro add tailwind

After running npx astro add react, Astro injects @astrojs/react into your config and adds the necessary peer deps. Your astro.config.mjs will look like this:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';

export default defineConfig({
  integrations: [react(), tailwind()],
  output: 'static', // or 'server' for SSR mode
});

In practice, you'll keep most of your UI in .astro components (which have zero runtime cost) and only pull in React for the pieces that genuinely need state, effects, or complex event handling. If you're building Empire UI-style components with glassmorphism cards or animated backgrounds, those static display pieces belong in .astro files - no hydration needed.

Performance Numbers: What You Actually Gain

Let me be concrete about what this buys you. A typical Next.js 14 marketing page ships somewhere between 80–200 kB of JavaScript (gzipped) just for the React runtime and hydration code, before you add a single line of your own logic. An equivalent Astro page with zero interactive islands ships 0 kB of JavaScript. With two or three islands (say, a navbar dropdown, a testimonial carousel, and a contact form), you're looking at 15–40 kB total.

Honestly, the Lighthouse scores speak for themselves. Astro consistently scores 95–100 on Performance for content-heavy pages because there's no hydration bottleneck blocking interactivity. The Time to Interactive metric - the one that actually correlates with user frustration - drops dramatically when you're not asking the browser to hydrate a 150 kB bundle before rendering a nav link.

Quick aside: partial hydration also benefits your Core Web Vitals in ways that matter for SEO. Lower Total Blocking Time (TBT) and better First Input Delay (FID) come almost for free when your JS budget per page is measured in kilobytes rather than megabytes. If you're already thinking about Lighthouse audits and bundle size, the bundlesize analysis guide pairs well with this.

The catch is real though - if you need a highly interactive UI (think dashboards with real-time data, collaborative editors, drag-and-drop builders), Astro is more friction than it's worth. Use Next.js App Router with React Server Components instead. Astro wins on content-heavy pages: marketing sites, blogs, documentation, landing pages, e-commerce product pages.

Sharing State Between Islands

Here's the part tutorials gloss over: islands are isolated by design, which means they don't share a React context tree. If you have a cart island and a header island both needing to know how many items are in the cart, you can't just prop-drill or context-share between them. Each island is its own independent React root.

The recommended pattern in 2026 is nano-stores - specifically the nanostores library, which Astro's own docs recommend. It's 286 bytes gzipped, framework-agnostic, and works across React, Vue, Svelte, and plain JS islands on the same page.

// src/stores/cart.ts
import { atom, computed } from 'nanostores';

export const cartItems = atom<CartItem[]>([]);

export const cartCount = computed(cartItems, (items) =>
  items.reduce((sum, item) => sum + item.quantity, 0)
);

export function addToCart(item: CartItem) {
  cartItems.set([...cartItems.get(), item]);
}
// src/components/CartIcon.tsx (React island)
import { useStore } from '@nanostores/react';
import { cartCount } from '../stores/cart';

export function CartIcon() {
  const count = useStore(cartCount);
  return (
    <button className="relative">
      <span>Cart</span>
      {count > 0 && (
        <span className="absolute -top-1 -right-1 bg-violet-600 text-white
          text-xs rounded-full w-5 h-5 flex items-center justify-center">
          {count}
        </span>
      )}
    </button>
  );
}

Both CartIcon and AddToCartButton islands subscribe to the same cartCount atom, and updates propagate instantly - no prop drilling, no global React context, no performance penalty. This pattern scales well. For anything more complex (auth state, server-synced data), reach for nanostores/persistent or wire up a lightweight API layer.

SSR Mode and Hybrid Rendering

Static output is Astro's default, but output: 'server' flips it to full SSR. Since Astro 2.0, there's also output: 'hybrid' - individual pages opt into SSR with export const prerender = false, everything else stays static. This is the mode you actually want for most real projects.

---
// src/pages/dashboard.astro
export const prerender = false; // This page is server-rendered

import { getSession } from '../lib/auth';
const session = await getSession(Astro.request);
if (!session) return Astro.redirect('/login');

const user = await fetchUser(session.userId);
---

<Layout>
  <h1>Welcome, {user.name}</h1>
  <!-- Static content above, interactive island below -->
  <DashboardWidget client:load userId={user.id} />
</Layout>

In practice, hybrid mode lets you keep your marketing pages as blazing-fast static HTML while serving authenticated routes dynamically. Your /pricing page? Static, cached on the CDN, zero server cost. Your /dashboard? SSR on every request with fresh data. You get both without switching frameworks.

Worth noting: Astro's SSR adapters cover all the major deployment targets - Vercel, Netlify, Cloudflare Workers, Node.js, Deno. The @astrojs/vercel adapter even supports Edge Functions out of the box, which means your SSR pages can run at the edge with sub-50ms latency globally. Check out the Vercel vs Netlify comparison if you're still deciding where to deploy.

Look, if you're building on Astro and want to pair it with a polished component library, Empire UI components work perfectly as Astro islands - just import any React component from Empire UI, drop it in your .astro file with a client:* directive, and you're done. The glassmorphism components and gradient generator especially shine in Astro's static-HTML-first model since the visual styles are pure CSS - zero JavaScript overhead.

When to Use Astro vs. Next.js in 2026

The nextjs-vs-astro debate has a cleaner answer now than it did two years ago. Astro owns the content-site category: blogs, docs, marketing pages, e-commerce storefronts, portfolios. Next.js owns the application category: dashboards, SaaS products, anything with complex client-side state and real-time UI. The overlap in the middle is where most of the confusion lives.

A good rule of thumb: count your interactive islands. If a page has fewer than 5 components that genuinely need JavaScript, Astro will serve it better. If most of the UI is stateful and interactive, Next.js App Router with React Server Components gives you a more ergonomic model. The Next.js vs Astro comparison goes deeper on the tradeoffs if you want the full breakdown.

One underrated Astro strength: content collections. Since Astro 2.0, you get type-safe Markdown and MDX with automatic frontmatter validation. For dev blogs, documentation sites, or anything where non-developers need to author content, content collections plus MDX components is an excellent stack. You get the author experience of a CMS with the performance of static HTML.

Astro's islands architecture isn't a silver bullet - it's a deliberately narrow tool optimized for one problem: shipping the least possible JavaScript for content-heavy pages. In that domain, it's the best option available in 2026. Use it for what it's good at, reach for React performance optimization techniques when you're back in Next.js territory, and don't let anyone tell you there's one framework to rule them all.

FAQ

What is the difference between client:load and client:idle in Astro?

client:load hydrates the component immediately when the page loads - use it for above-the-fold interactive elements. client:idle waits until the browser's idle callback fires, reducing competition with critical rendering work. Default to client:idle unless the component must be ready on first paint.

Can I mix React and Svelte components in the same Astro project?

Yes, and it actually works well. Astro ships only the framework runtime needed for each island, so a React component and a Svelte component on the same page each get their own isolated bundle. You do lose the ability to share React context or Svelte stores between them - use nanostores for cross-island state instead.

Does Astro support server-side rendering or only static sites?

Astro supports static output, full SSR, and hybrid mode (static pages by default, individual pages opt into SSR). Hybrid mode is the most practical for real projects - most pages stay static and CDN-cached while authenticated or data-heavy routes render on the server.

Can I use Empire UI components in an Astro project?

Yes. Add the React integration with npx astro add react, import any Empire UI component in your .astro file, and attach a client:* directive for components that need interactivity. Pure display components (cards, typography, layout) can be rendered without any directive for zero JavaScript output.

Free components in 41 styles
React & Tailwind, copy-paste ready.
Browse β†’

Read next

Advanced CSS & JavaScript Patterns: Production-Grade Techniques 2026 β†’Astro + React: Using React Components in an Astro Site β†’Qwik Framework: Resumability, Lazy Loading and Zero Hydration β†’Next.js vs Astro in 2026: Which Framework Fits Your Project? β†’