The web development industry has been searching for the holy grail: the performance of multi-page applications with the interactivity of single-page applications. Islands architecture, pioneered by Katie Sylor-Miller and formalized by Jason Miller, offers exactly this compromise.
The Evolution of Web Rendering
Multi-Page Applications (MPA)
Traditional websites rendered everything on the server. Each navigation triggered a full page reload. While simple and reliable, this approach felt slow and disconnected.
Single-Page Applications (SPA)
React, Vue, and Angular introduced client-side routing and state management. The page loads once, then JavaScript handles navigation. This felt fast but introduced significant JavaScript bloat and initial load delays.
Server-Side Rendering (SSR)
Next.js, Nuxt, and SvelteKit brought back server rendering but combined it with client-side hydration. The page renders on the server, then JavaScript “hydrates” it on the client. This improved initial load but still required downloading and executing significant JavaScript.
The Hydration Tax
Even with SSR, the browser must:
- Download all JavaScript for the page
- Parse and compile the JavaScript
- Execute JavaScript to attach event listeners
- Rebuild component state
This process—hydration—can take seconds on mobile devices, especially for content-heavy pages like blogs and documentation sites.
What is Islands Architecture?
Islands architecture flips the default: static content stays static, while interactive components become “islands” of JavaScript in a sea of HTML.
Core Principles
- Default to Static: The page is primarily static HTML
- Islands of Interactivity: Only interactive components include JavaScript
- Independent Hydration: Each island hydrates independently
- Parallel Execution: Islands hydrate in parallel, not sequentially
Visual Representation
Traditional SSR:
[HTML] + [JavaScript Bundle: 500KB]
↓ Hydrate everything
[Interactive Page]
Islands Architecture:
[Static HTML] + [Island 1: 2KB] + [Island 2: 5KB] + [Island 3: 3KB]
↓ Hydrate only islands
[Interactive Page]
How Islands Work
Server-Side Rendering
The server renders all content to HTML, including interactive components. Interactive components include markers indicating their boundaries:
<!-- Static content -->
<header>My Blog</header>
<article>
<h1>Understanding Islands</h1>
<p>Islands architecture is revolutionary...</p>
</article>
<!-- Interactive island -->
<my-counter data-server-value="0">
<button>Count: 0</button>
</my-counter>
<!-- More static content -->
<footer>Copyright 2026</footer>
Client-Side Hydration
The browser receives HTML and:
- Renders static content immediately (no JavaScript needed)
- Identifies islands by their markers
- Downloads JavaScript only for each island
- Hydrates islands independently in parallel
// Each island hydrates independently
// Island 1: Counter - 2KB downloaded, executed
// Island 2: Search - 5KB downloaded, executed
// Island 3: Comments - 3KB downloaded, executed
// They don't wait for each other
// They don't share a hydration cycle
Implementation in Astro
Astro is the most popular framework implementing islands architecture.
Basic Example
---
// src/pages/index.astro
import Counter from '../components/Counter.jsx';
import Search from '../components/Search.jsx';
import Comments from '../components/Comments.jsx';
---
<html>
<head>
<title>Islands Demo</title>
</head>
<body>
<!-- Static header - no JavaScript -->
<header>
<h1>My Blog</h1>
<nav>...</nav>
</header>
<!-- Interactive island - loads JavaScript -->
<Search client:load />
<!-- Interactive island - hydrates when visible -->
<Comments client:visible />
<!-- Interactive island - hydrates on idle -->
<Counter client:idle />
<!-- Static footer - no JavaScript -->
<footer>...</footer>
</body>
</html>
Client Directives
Astro provides several directives controlling when islands hydrate:
<!-- client:load - Hydrate immediately on page load -->
<Search client:load />
<!-- client:visible - Hydrate when scrolled into view -->
<Comments client:visible />
<!-- client:idle - Hydrate when browser is idle -->
<Counter client:idle />
<!-- client:media - Hydrate when media query matches -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- client:only - Render only on client, skip server -->
<NonSSRLibrary client:only="react" />
Component Integration
Astro supports multiple UI frameworks in the same project:
---
import ReactCounter from '../components/ReactCounter.jsx';
import VueSearch from '../components/VueSearch.vue';
import SvelteCart from '../components/SvelteCart.svelte';
import PreactButton from '../components/PreactButton.jsx';
---
<!-- Mix frameworks freely -->
<ReactCounter client:visible />
<VueSearch client:load />
<SvelteCart client:idle />
<PreactButton client:visible />
Real-World Performance Impact
Typical Page Comparison
| Metric | Traditional SSR | Islands Architecture |
|---|---|---|
| JavaScript | 450KB | 15KB |
| Time to Interactive | 3.2s | 0.8s |
| First Input Delay | 150ms | 10ms |
| Total Blocking Time | 1200ms | 50ms |
When Islands Shine
- Content sites: Blogs, documentation, marketing pages
- E-commerce: Product listings, category pages
- Dashboards: Mix of static data and interactive widgets
When Islands May Not Fit
- Highly interactive web applications
- Real-time collaborative apps
- Complex state-dependent UIs
Building Custom Islands
Creating an Island Component
// components/NewsletterSignup.jsx
import { useState } from 'react';
export default function NewsletterSignup() {
const [email, setEmail] = useState('');
const [subscribed, setSubscribed] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify({ email })
});
setSubscribed(true);
};
if (subscribed) {
return <p>Thanks for subscribing!</p>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
/>
<button type="submit">Subscribe</button>
</form>
);
}
Using in Astro
---
import NewsletterSignup from '../components/NewsletterSignup.jsx';
---
<section>
<h2>Newsletter</h2>
<!-- Only this component includes JavaScript -->
<NewsletterSignup client:visible />
</section>
Framework Support
Astro
The pioneer and most mature implementation:
npm create astro@latest
Marko
Facebook’s legacy framework that pioneered similar concepts:
<app>
<static-content/>
<interactive-component state="new"/>
</app>
Qwik
Built-in resumability achieves similar goals:
<button onClick$={() => count.value++}>
{count.value}
</button>
<!-- Automatically lazy-loaded -->
Svelte 5
Runes enable fine-grained reactivity:
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
{count}
</button>
<!-- Compiles to tiny, targeted JavaScript -->
Best Practices
Identify True Islands
Not every component needs to be an island:
<!-- DON'T: Over-island -->
<Button client:load>Click</Button>
<Card client:load>Content</Card>
<!-- DO: Only interactive components -->
<Search client:load />
<Newsletter client:visible />
<Comments client:visible />
Choose Hydration Strategies Wisely
<!-- Navigation - needs immediate interactivity -->
<MobileMenu client:load />
<!-- Below-fold content - can wait -->
<Comments client:visible />
<!-- Heavy components - wait until idle -->
<AnalyticsChart client:idle />
<!-- Third-party widgets - often best client:only -->
<ChatWidget client:only="react" />
Minimize Island Size
// BAD: Large component bundle
export default function Comments() {
const { Editor, Viewer, Parser, User } = await import('heavy-lib');
// 200KB of JavaScript
}
// GOOD: Lazy-load heavy imports
export default function Comments() {
const [showEditor, setShowEditor] = useState(false);
return (
<>
<Viewer data={data} />
{showEditor && <Editor onSave={save} />}
<button onClick={() => setShowEditor(true)}>
Edit
</button>
</>
);
}
Advanced Patterns
Nested Islands
---
import Parent from '../components/Parent.jsx';
import Child from '../components/Child.jsx';
---
<!-- Parent hydrates, renders Child as prop -->
<Parent client:load>
<Child client:visible />
</Parent>
Islands Communication
---
import { signal } from '@preact/signals';
import Search from '../components/Search.jsx';
import Results from '../components/Results.jsx';
// Shared signal
export const searchQuery = signal('');
---
<Search client:load />
<Results client:visible />
Conditional Islands
---
const isLoggedIn = true;
---
{isLoggedIn ? (
<Dashboard client:load />
) : (
<LoginForm client:load />
)}
Debugging Islands
Viewing Island Hydration
Use browser DevTools:
// Console: See hydration timing
window.__ASTRO__?.islands?.forEach(island => {
console.log(
island.componentUrl,
island.hydrationTime
);
});
Performance Profiling
// Performance panel shows:
// - Island hydration start/end
// - JavaScript download timing
// - Hydration duration per island
Resources
- Astro Documentation
- Jason Miller’s Islands Article
- Katie Sylor-Miller’s Blog
- Islands Architecture Explained
Conclusion
Islands architecture represents a pragmatic middle ground in the ongoing debate between static and dynamic web applications. By accepting that most content is static and only some components need interactivity, we can deliver dramatically better user experiences.
The pattern has proven production-ready through Astro’s growing adoption and the success of similar implementations in Qwik and Svelte 5. For content-heavy sites in 2026, islands architecture should be the default choice.
The key insight is simple: don’t pay for JavaScript you don’t need. Static content doesn’t need hydration. Interactive components shouldn’t wait for non-interactive content. Islands architecture makes this distinction explicit and automatic.
Comments