Islands Architecture and Partial Hydration — Building Interactive Websites with 90% Less JS Using Astro
Prerequisites: If you've used Next.js or React SSR at least once, you can follow along right away.
Have you ever felt that your pages were still slow even though you were doing SSR with Next.js — rendering HTML on the server and yet something still felt off? I used to think "SSR means it's fast, right?" but it turned out the cost of re-hydrating the entire page on the client after SSR was no small thing. Just to show a single blog post, you have to download and execute all the JS for the comment section, the recommended posts slider, social share buttons, and more. What a waste.
Once you recognize the problem, the solution is actually straightforward: leave most of the page as plain static HTML, and attach JS only to the parts that genuinely need interactivity. This is the core idea behind Islands Architecture. In fact, Wix adopted this kind of Selective Hydration and improved interaction speed by 40%. This article covers everything in one place — how Islands Architecture works, how to implement it in practice with Astro, and the pitfalls you'll encounter in real projects.
First proposed by Etsy's Katie Sylor-Miller in 2019 and formalized by Preact creator Jason Miller, this pattern has become a frontend mainstream in 2025, led by Astro, Qwik, and Fresh. React Server Components also share a similar direction in that they explicitly separate server/client boundaries, and the industry as a whole seems to be converging in this direction.
Core Concepts
What Is Hydration, and Why Is It a Problem?
When doing web development, you frequently encounter the word "Hydration."
Hydration: The process of attaching JavaScript event listeners and state to server-rendered static HTML to make it interactive. Full Hydration does this for the entire page, while Partial Hydration performs this only for the components that need it.
The problem is that existing SSR frameworks hydrate the entire page. Take a blog post page as an example — the title, body, and publish date are static content that never changes, yet because "this page uses React," the entire JS bundle must be downloaded, parsed, and executed. What a waste.
The Idea Behind Islands Architecture
The concept itself is intuitive. Think of a page as a sea of static HTML — components that need interactivity are islands floating on that sea. Each island hydrates independently, while the rest of the sea remains plain HTML.
| Approach | Hydration Scope | JS Payload |
|---|---|---|
| SPA (Create React App, etc.) | 100% of the page | Very large |
| SSR + Full Hydration (Next.js default) | Full re-hydration after server render | Large (improving with RSC) |
| Islands Architecture | Interactive components only | Minimal |
Note that Next.js 13+ App Router has significantly reduced bundle sizes through RSC, so even the Full Hydration approach ships much less JS than before. That said, for content-heavy pages where 80–90% can be static HTML and only the remaining 10–20% — comment sections, shopping carts, search bars — need to be treated as islands, Islands Architecture still has the edge in bundle optimization.
Partial Hydration: When Should an Island Wake Up?
Partial Hydration is the core mechanism for implementing Islands Architecture, and Astro's client:* directives are the best expression of it. They let you declaratively specify when each component should load JS and become active.
Each island component is split into its own JS chunk, and when the right moment comes, only the needed code is loaded via browser APIs like IntersectionObserver (viewport entry detection) or requestIdleCallback (idle time detection). Rather than downloading the entire page bundle at once, each island activates individually when it's needed.
| Directive | Hydration Trigger | Suitable For |
|---|---|---|
client:load |
Immediately on page load | Search bars, navigation |
client:idle |
When the browser is idle | Social share buttons |
client:visible |
When entering the viewport | Comment sections, bottom recommended content |
client:media |
When a media query is matched | Mobile-only menus |
When I first saw this table in practice, I thought "Can't I just use client:load for everything?" — but after writing the code below myself, I finally understood how important it is to use the right directive.
Practical Application
Example 1: Building a Blog Post Page with Astro
This is the most representative use case. The blog post body never needs JS, but the comment section and search bar require interactivity.
---
// src/pages/posts/[slug].astro
import CommentSection from '../../components/CommentSection.tsx';
import SearchBar from '../../components/SearchBar.tsx';
import ShareButton from '../../components/ShareButton.tsx';
const { post } = Astro.props;
---
<html>
<body>
<!-- Static area: no JS at all, fast FCP guaranteed -->
<h1>{post.title}</h1>
<p class="meta">{post.date} · {post.author}</p>
<article set:html={post.content} />
<!-- Island: hydrated on viewport entry (no JS needed until scrolled) -->
<CommentSection client:visible postId={post.id} />
<!-- Island: hydrated immediately on page load (search should be ready right away) -->
<SearchBar client:load />
<!-- Island: hydrated when browser is idle (not urgent) -->
<ShareButton client:idle url={post.url} />
</body>
</html>Here's a summary of which directive was applied to each component and why:
| Component | Directive | Reason |
|---|---|---|
CommentSection |
client:visible |
Only visible after scrolling — no need to load early |
SearchBar |
client:load |
Users must be able to use it immediately |
ShareButton |
client:idle |
Low priority — can be handled when the main thread is free |
Example 2: Sharing State Between Islands — Using Nano Stores
Honestly, this is where you get stuck when you first try Islands Architecture. Because islands are independent, reflecting a quantity change in the cart island back to the header badge island is trickier than you'd expect. I remember wasting a lot of time trying to solve it with prop drilling.
This is where the lightweight state management library Nano Stores comes in handy. Nano Stores' atom is the smallest unit of observable value — when islands subscribe to the same atom, any change by one automatically propagates to the others.
// src/stores/cart.ts — shared state between islands
import { atom, computed } from 'nanostores';
export interface CartItem {
id: string;
name: string;
quantity: number;
price: number;
}
export const cartItems = atom<CartItem[]>([]);
// Total quantity derived from cartItems
export const cartCount = computed(cartItems, (items) =>
items.reduce((sum, item) => sum + item.quantity, 0)
);
export function addToCart(item: CartItem) {
const current = cartItems.get();
const existing = current.find((i) => i.id === item.id);
if (existing) {
// If the item already exists, just increment the quantity
cartItems.set(
current.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
)
);
} else {
// Otherwise, add it as a new item
cartItems.set([...current, { ...item, quantity: 1 }]);
}
}// src/components/CartBadge.tsx — island mounted in the header
import { useStore } from '@nanostores/react';
import { cartCount } from '../stores/cart';
export default function CartBadge() {
const count = useStore(cartCount);
return (
<div className="cart-badge">
🛒 {count > 0 && <span className="badge">{count}</span>}
</div>
);
}// src/components/ProductCard.tsx — island in the product listing
import { addToCart, type CartItem } from '../stores/cart';
interface Props {
product: CartItem;
}
export default function ProductCard({ product }: Props) {
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()} won</p>
{/* Clicking this automatically updates CartBadge */}
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
);
}Because both islands subscribe to the same store, calling addToCart in ProductCard automatically triggers a reaction in CartBadge — cleanly connected without global context or prop drilling.
Pros and Cons Analysis
Here's a summary of the pros and cons I experienced firsthand:
Advantages
| Item | Description | Suitable Scenarios |
|---|---|---|
| Minimal JS bundle | Only ships code for interactive areas, dramatically improving initial load speed | Content-heavy pages |
| Improved Core Web Vitals | Better TTI and INP metrics directly impact SEO and ad revenue | Services where search traffic matters |
| Multi-framework support | Mix React islands and Svelte islands on the same page (Astro) | During tech stack migrations |
| Progressive enhancement | Static content remains accessible even when JS is disabled | Services with accessibility requirements |
| CDN caching efficiency | Higher ratio of static HTML means better CDN cache hit rates | High-traffic public pages |
Disadvantages and Caveats
| Item | Description | Mitigation |
|---|---|---|
| State sharing between islands | The independent nature of islands makes global state management complex | Using lightweight tools like Nano Stores resolves this |
| Limited applicability | Not suitable for complex SPAs like dashboards or real-time collaboration tools | Assess the nature of the service before adopting |
| Ecosystem maturity | Gradually applying it to existing React/Next.js projects is not straightforward | Realistically better to start with a new project using dedicated frameworks like Astro or Fresh |
| Design mindset shift | "What counts as an island?" must be decided early in the design phase | Start by asking "can this component work without JS?" for each component |
| Real-time data | Fully static pages require additional strategies for real-time data updates | Combine with Server Islands or a separate API call strategy |
TTI (Time to Interactive): The time until a page becomes fully interactive. A long TTI means there's a window where users click buttons but get no response. INP (Interaction to Next Paint) replaced FID as a Core Web Vitals metric in 2024, measuring the delay between a user interaction and the next screen update. Both metrics directly affect SEO rankings.
The Most Common Mistakes in Practice
-
Turning every component into an island — If you create islands liberally thinking "maybe I'll add interactivity later," you end up no better off than Full Hydration. Stick to "does this need JS right now?" as a strict criterion.
-
Attempting direct communication between islands — Because islands mount independently, trying to communicate between them via parent-child prop passing either won't work or will produce unexpected state inconsistencies. Always use the shared store pattern for inter-island communication.
-
Using
client:loadas the default — Applyingclient:loadto every island eliminates more than half the benefits of partial hydration. For components like comment sections that only appear after scrolling,client:visibleis far more appropriate.
Wrapping Up
Islands Architecture is a powerful pattern that starts from the question "how little JS can we use?" and makes content-driven websites significantly faster and more efficient. Of course it's not right for every service, but for blogs, documentation, marketing sites, and e-commerce product pages, it's well worth considering.
Three steps you can take right now:
- Build a simple blog page with Astro — Create a project with
pnpm create astro@latest, attach your existing React components withclient:visible, and you'll feel the difference immediately. - Classify your current service's components as "static" or "dynamic" — Ask "can users still see the content if JS is removed from this component?" for each component on screen. You'll discover that more than you'd expect can be handled statically.
- Identify the points that need shared state between islands upfront — Practice introducing Nano Stores for shared state like cart quantities and login status, and you'll find actual project adoption much smoother.
Next article: Deep dive into Astro Server Islands — how to move personalized content and authenticated areas outside the main render path to maximize CDN cache hit rates.
References
- Islands Architecture | Jason Miller — Written by the originator of the concept himself; the clearest explanation of the background and core principles.
- Islands Architecture | Astro Docs — Great for understanding how
client:*directives work and Astro's implementation philosophy. - Islands Architecture | Patterns.dev — Compares the approach with SPA/SSR using diagrams and provides a neutral perspective on the overall pattern.
- Understanding Astro Islands Architecture | LogRocket — Suitable when you want to learn by following a real Astro project.
- Astro Islands Architecture Explained | Strapi — Includes scenarios combining a CMS with Islands, useful reference for building content sites.
- Modern Front-End Architecture Using Islands and Partial Hydration | NamasteDev — Worth reading when you want a deeper understanding of how partial hydration works.
- Partial Hydration: The End of Slow Websites | Feature-Sliced Design — An interesting perspective on Islands through the lens of FSD architecture.
- 40% Faster Interaction: Wix Selective Hydration Case Study | Wix Engineering — A production-proven case with real numbers; great material for making the case internally.
- Why Islands Architecture Is the Future | DEV Community — Covers the future outlook for Islands Architecture from a community perspective.