Island Architecture and Partial Hydration
The JavaScript Tax
Here's a number that should bother you: the median web page ships 500KB of JavaScript. Most of that JavaScript is framework runtime — React, its reconciler, the virtual DOM diffing algorithm — all executing just to produce the same HTML the server already rendered.
On a typical content page — a blog post, docs page, or marketing landing page — maybe 5% of the page is actually interactive. A nav menu toggle, a code copy button, a theme switcher. The other 95% is static text, images, and headings that never change after the first render. Yet traditional SPA frameworks hydrate the entire page, re-executing every component just to attach a few event listeners.
Island architecture asks a radical question: what if we only shipped JavaScript for the parts that actually need it?
The Mental Model
Picture a page as an ocean of static HTML with small islands of interactivity scattered across it.
The ocean — your headings, paragraphs, images, navigation structure — is pure HTML. It doesn't need JavaScript. It rendered on the server, arrived in the browser as HTML, and it's done.
The islands — a search widget, an interactive chart, a comment form — are the only parts that need JavaScript. Each island hydrates independently, with its own framework bundle, its own state, its own lifecycle. The islands don't know about each other.
Traditional SPA frameworks treat the entire page as one giant island. Island architecture recognizes that most pages are 90% ocean.
Astro: Islands in Practice
Astro is the framework that popularized the islands pattern. By default, Astro components ship zero JavaScript. You opt in to interactivity per component using client directives.
---
// This is an Astro component (runs on server only, ships zero JS)
import Header from '../components/Header.astro'
import BlogContent from '../components/BlogContent.astro'
import SearchWidget from '../components/SearchWidget.tsx' // React component
import ThemeToggle from '../components/ThemeToggle.svelte' // Svelte component
import Newsletter from '../components/Newsletter.vue' // Vue component
---
<Header /> <!-- Static HTML, zero JS -->
<BlogContent /> <!-- Static HTML, zero JS -->
<SearchWidget client:load /> <!-- Hydrates immediately on page load -->
<ThemeToggle client:idle /> <!-- Hydrates when browser is idle -->
<Newsletter client:visible /> <!-- Hydrates when scrolled into view -->
Notice something wild: React, Svelte, and Vue components on the same page. Each island uses whatever framework makes sense for that component. The static parts use Astro's own zero-JS template syntax.
Client Directives
The client: directives control when an island hydrates:
client:load → Hydrate immediately on page load
Use for: above-the-fold interactive elements
client:idle → Hydrate when the browser is idle (requestIdleCallback)
Use for: non-critical interactive elements
client:visible → Hydrate when the component scrolls into view (IntersectionObserver)
Use for: below-the-fold components like comment sections
client:media → Hydrate when a media query matches
Use for: mobile-only or desktop-only interactive components
Example: client:media="(max-width: 768px)"
client:only → Skip SSR entirely, render only on the client
Use for: components that access browser-only APIs
Example: client:only="react"
The Performance Math
Let's compare a typical docs page in Next.js (SPA) vs Astro (islands):
Next.js SPA approach:
─────────────────────
HTML payload: 15 KB
JS bundle: 85 KB (React runtime + page components + hydration)
Total transfer: 100 KB
Time to Interactive: 1.2s (must download, parse, execute 85KB JS)
Astro islands approach:
───────────────────────
HTML payload: 15 KB
JS bundle: 8 KB (only the search widget's React island)
Total transfer: 23 KB
Time to Interactive: 0.3s (only 8KB JS to parse)
That's a 77% reduction in JavaScript. And the page is interactive 4x faster. The 92% of the page that's static content didn't need JavaScript to begin with.
Independent Hydration
Each island hydrates independently — it doesn't wait for other islands:
Traditional SPA hydration:
|─── Download full JS bundle ───|─── Parse ───|─── Hydrate entire page ───|
0ms 200ms 350ms 600ms
All interactive
Island hydration:
|─ Download search JS (8KB) ─|─ Hydrate search ─|
0ms 50ms 80ms ← Search interactive
|─ Download theme JS (3KB) ─|─ Hydrate ─|
100ms (idle) 130ms 140ms ← Theme toggle interactive
|─ Download newsletter JS (5KB) ─|─ Hydrate ─|
User scrolls to newsletter 500ms 520ms
Each island becomes interactive as soon as its own JavaScript loads — independent of every other island on the page.
How islands maintain isolation
Each island is a self-contained hydration root. Astro renders the component to HTML on the server, wraps it in a custom element (astro-island), and includes a small script that bootstraps the framework for that specific component.
<!-- What Astro outputs for a React island -->
<astro-island
uid="abc123"
component-url="/_astro/SearchWidget.Bx3kL.js"
component-export="SearchWidget"
renderer-url="/_astro/client.react.Dk9f.js"
props='{"placeholder":"Search docs..."}'
client="load"
>
<!-- Server-rendered HTML of SearchWidget -->
<div class="search-widget">
<input placeholder="Search docs..." />
</div>
</astro-island>The astro-island custom element handles:
- Loading the component's JavaScript module
- Loading the framework renderer (React, Svelte, etc.)
- Hydrating the component with its serialized props
- All scoped to this single component — no global framework state
This is why you can mix React, Svelte, and Vue on the same page. Each island brings its own renderer, and they never interfere with each other.
Server Islands
Astro also supports server islands — components that render dynamically on the server while the rest of the page is served from the CDN cache:
---
import UserGreeting from '../components/UserGreeting.astro'
import ProductGrid from '../components/ProductGrid.astro'
---
<!-- Static: cached on CDN -->
<h1>Our Products</h1>
<ProductGrid />
<!-- Server island: renders dynamically per request -->
<UserGreeting server:defer>
<p slot="fallback">Loading your profile...</p>
</UserGreeting>
Server islands solve the personalization-vs-caching dilemma. The 95% of the page that's the same for everyone is cached statically. The 5% that's personalized (user greeting, cart count, recommendations) renders dynamically on the server and gets injected into the static shell.
When Islands Architecture Wins (and When It Doesn't)
Islands win for:
- Content-heavy sites — blogs, documentation, marketing pages, news sites
- Multi-framework teams — one team uses React, another uses Svelte? Islands handle it
- Performance-critical sites — when every KB of JavaScript matters (mobile users, emerging markets)
- SEO-focused sites — full HTML in the initial response, minimal JS blocking
Islands don't work well for:
- Highly interactive apps — if 80%+ of the page is interactive, you're building many large islands that need to share state. At that point, you have an SPA with extra overhead
- Shared state between components — islands are isolated. Sharing state between them requires workarounds (custom events, URL params, shared stores)
- Complex client-side routing — islands are page-level. Deep SPA-style navigation with transitions between views is not the islands model
When to use what:
Interactive ratio | Best approach
< 20% | Islands (Astro)
20-50% | Hybrid (Next.js with RSC)
> 50% | Full SPA (Next.js, Remix)
Production Scenario: The Developer Documentation Site
A team migrates their Next.js-based developer docs to Astro. The site has 3,000 pages with these interactive elements:
- Global search widget (every page)
- Code block copy buttons (every page)
- Interactive API playground (50 pages)
- Theme toggle (every page)
- Feedback widget (every page)
Before (Next.js SPA):
- JS bundle per page: 120KB
- Time to Interactive: 1.8s on 4G
- Lighthouse Performance: 72
After (Astro Islands):
- JS per page: 12KB average (search + theme toggle + copy buttons)
- API playground pages: 45KB (larger island for the interactive editor)
- Time to Interactive: 0.4s on 4G
- Lighthouse Performance: 98
The 3,000 static content pages dropped from 120KB to 12KB of JavaScript each. The 50 API playground pages have more JS, but still 62% less than before because only the playground component ships JavaScript — not the entire page.
Common Mistakes
| What developers do | What they should do |
|---|---|
| Using `client:load` for every interactive component `client:load` hydrates immediately, adding to the critical path. If a component does not need to be interactive within the first second, defer its hydration to reduce main thread blocking during page load. | Use `client:idle` for non-critical elements and `client:visible` for below-the-fold components |
| Trying to share React state between separate islands Each island is an isolated hydration root with its own React instance. React context, state, and refs do not cross island boundaries. If two components need shared state, they should be in the same island. | Use framework-agnostic state sharing (nanostores, custom events, URL params) or merge related components into a single island |
| Building a highly interactive dashboard with islands architecture Islands architecture optimizes for pages with low interactivity ratios. If 80% of the page is interactive, you end up with many large islands that need workarounds to communicate, losing the simplicity and performance benefits. | Use a full SPA framework (Next.js, Remix) when most of the page is interactive |
| Assuming islands architecture means giving up SSR capabilities Astro is not static-only. You can configure routes as SSR with `output: 'server'` or use hybrid mode where most routes are static but specific routes render dynamically. | Astro supports full SSR, hybrid rendering (static + SSR per route), and server islands for per-request dynamic content |
Key Rules
- 1Island architecture renders pages as static HTML by default and selectively hydrates only the interactive components, shipping zero JavaScript for everything else.
- 2Client directives (client:load, client:idle, client:visible, client:media) control when each island hydrates, letting you prioritize critical interactivity.
- 3Each island is an isolated hydration root — it loads its own framework renderer and hydrates independently of other islands on the page.
- 4Islands can use different frameworks (React, Svelte, Vue) on the same page because each island bundles its own renderer.
- 5Server islands solve the personalization-vs-caching problem by rendering dynamic content on the server while the rest of the page is served from static cache.
- 6Islands architecture is best for content-heavy sites with low interactivity ratios (less than 20% interactive). For highly interactive apps, a full SPA framework is more appropriate.