Implementing Islands Architecture for a web application
Islands Architecture — architectural pattern where a page consists of static HTML ocean with isolated "islands" of interactivity. Each island is independent component with its own JS, hydrating separately, unaware of neighboring islands.
Jason Miller (Preact) described concept in 2020. Today it's foundation of Astro, Marko, and part of Qwik approach.
Architectural model
Static HTML (server, zero JS):
┌─────────────────────────────────────┐
│ Header │ ← HTML
│ Logo | Nav links | ... │
├─────────────────────────────────────┤
│ Hero section │ ← HTML
│ H1, image, CTA │
├──────────────┬──────────────────────┤
│ Article text │ 🏝️ Island: │ ← JS only for island
│ (HTML) │ TableOfContents.tsx │
│ │ (sticky, highlight) │
├──────────────┴──────────────────────┤
│ 🏝️ Island: CommentSection.tsx │ ← JS only for island
│ (React, loads on scroll) │
├─────────────────────────────────────┤
│ Footer │ ← HTML
└─────────────────────────────────────┘
Result: JS loads only for 2 islands, not entire page
Implementation in Astro
Astro is first framework with native Islands support:
---
// src/pages/blog/[slug].astro
import type { GetStaticPaths } from 'astro';
import { getCollection } from 'astro:content';
// Server components — .astro files with no client JS
import BaseLayout from '@/layouts/BaseLayout.astro';
import ArticleHero from '@/components/ArticleHero.astro';
import Prose from '@/components/Prose.astro';
// Islands — React/Vue/Svelte components with directives
import TableOfContents from '@/islands/TableOfContents.tsx';
import CommentSection from '@/islands/CommentSection.tsx';
import ShareButtons from '@/islands/ShareButtons.svelte';
import NewsletterSignup from '@/islands/NewsletterSignup.vue';
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getCollection('blog', p => !p.data.draft);
return posts.map(post => ({ params: { slug: post.slug }, props: { post } }));
};
const { post } = Astro.props;
const { Content, headings } = await post.render();
---
<BaseLayout title={post.data.title} description={post.data.description}>
<ArticleHero post={post} />
<div class="article-layout">
<!-- Island 1: interactive table of contents -->
<!-- Loads only when browser idle -->
<TableOfContents headings={headings} client:idle />
<article>
<Prose>
<Content />
</Prose>
</article>
</div>
<!-- Island 2: share buttons -->
<!-- Loads only when visible in viewport -->
<ShareButtons
url={Astro.url.href}
title={post.data.title}
client:visible
/>
<!-- Island 3: comments -->
<!-- Loads only when visible in viewport, 500ms delay -->
<CommentSection
articleId={post.id}
client:visible={{ rootMargin: '0px 0px 200px 0px' }}
/>
<!-- Island 4: newsletter signup -->
<!-- Loads immediately — in fold -->
<NewsletterSignup client:load />
</BaseLayout>
Inter-island communication
Islands are isolated — they have no shared React context. For communication use:
Nano Stores (recommended for Astro):
// src/stores/cart.ts
import { atom, computed } from 'nanostores';
import { persistentAtom } from '@nanostores/persistent';
export const cartItems = persistentAtom<CartItem[]>('cart', [], {
encode: JSON.stringify,
decode: JSON.parse,
});
export const cartCount = computed(cartItems, items => items.length);
export const cartTotal = computed(cartItems, items =>
items.reduce((sum, item) => sum + item.price * item.qty, 0)
);
export function addToCart(product: Product) {
const items = cartItems.get();
const existing = items.find(i => i.id === product.id);
if (existing) {
cartItems.set(items.map(i => i.id === product.id ? { ...i, qty: i.qty + 1 } : i));
} else {
cartItems.set([...items, { ...product, qty: 1 }]);
}
}
// islands/CartIcon.tsx — React island
import { useStore } from '@nanostores/react';
import { cartCount } from '@/stores/cart';
export function CartIcon() {
const count = useStore(cartCount);
return (
<a href="/cart" className="relative">
<ShoppingCartIcon />
{count > 0 && <span className="badge">{count}</span>}
</a>
);
}
<!-- islands/AddToCartButton.svelte — Svelte island -->
<script>
import { addToCart } from '@/stores/cart';
export let product;
let loading = false;
async function handleAdd() {
loading = true;
addToCart(product);
loading = false;
}
</script>
<button on:click={handleAdd} disabled={loading}>
{loading ? 'Adding...' : 'Add to cart'}
</button>
Both islands (React and Svelte) work with one store. Change in one immediately reflects in other.
Native browser events:
// Universal event bus via CustomEvent
export function emit<T>(event: string, detail: T) {
window.dispatchEvent(new CustomEvent(event, { detail, bubbles: true }));
}
export function on<T>(event: string, handler: (detail: T) => void) {
const wrapped = (e: CustomEvent<T>) => handler(e.detail);
window.addEventListener(event, wrapped as EventListener);
return () => window.removeEventListener(event, wrapped as EventListener);
}
// In island 1
emit('product:added', { id: product.id, name: product.name });
// In island 2
useEffect(() => {
return on<{ id: string; name: string }>('product:added', ({ name }) => {
showToast(`${name} added to cart`);
});
}, []);
Rendering islands on multiple frameworks
Astro supports multiple renderer plugins simultaneously:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';
import preact from '@astrojs/preact';
export default defineConfig({
integrations: [
react(), // For existing React components
vue(), // For Vue components from library
svelte(), // For light interactive widgets
preact({ compat: true }), // Preact as React replacement for light islands
],
});
In practice: different frameworks for different purposes within one site without conflicts.
Performance: numbers
Measurements on real projects after Islands Architecture migration:
| Metric | Before (Full React SSR) | After (Astro Islands) |
|---|---|---|
| Initial JS | 180–250 KB | 15–40 KB |
| TTI | 3.2s | 0.8s |
| TBT | 480ms | 60ms |
| Lighthouse Performance | 62 | 96 |
Variance depends on island count and complexity.
Pattern limitations
Islands Architecture doesn't fit:
- SPA applications — need unified state across many components
- Dashboards — too much interactivity, islands lose isolation
- Pages where everything is interactive — minimal benefit
Ideal case: content site with few interactive blocks per page.
Implementation timeline
- Week 1–2: audit existing site, identify interactive components, setup Astro + renderer plugins
- Week 3: migrate static pages, split into islands with hydration directives
- Week 4: inter-island communication via nano stores, test isolation
- Week 5: measure Core Web Vitals, compare with baseline, optimize directives
- Week 6: deployment (Cloudflare Pages / Netlify), Lighthouse CI, final documentation







