Rendering
Islands Architecture
Interactive components in a sea of static HTML
Intermediate
hydration astro performance architecture
Definition
Islands Architecture is a web development pattern where interactive UI components (islands) are hydrated independently within a sea of static, non-interactive server-rendered HTML. Each island hydrates in isolation, reducing JavaScript overhead while maintaining interactivity where needed.
The Concept
Traditional SPA: Islands Architecture:
┌─────────────────────────┐ ┌─────────────────────────┐
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ │ │ │ │ 🏝️ Island 1 │ │
│ │ JavaScript │ │ │ │ (Interactive) │ │
│ │ Everywhere │ │ │ └─────────────────────┘ │
│ │ │ │ │ │
│ │ │ │ │ Static HTML │
│ │ │ │ │ (No JS) │
│ │ │ │ │ │
│ └─────────────────────┘ │ │ ┌─────────────────────┐ │
└─────────────────────────┘ │ │ 🏝️ Island 2 │ │
│ │ (Interactive) │ │
│ └─────────────────────┘ │
│ │
│ Static HTML │
│ │
│ ┌─────────────────────┐ │
│ │ 🏝️ Island 3 │ │
│ │ (Interactive) │ │
│ └─────────────────────┘ │
└─────────────────────────┘
How It Works
1. Server Rendering
---
// Astro example
// This runs on server only
const data = await fetch('https://api.example.com/posts');
---
<!-- Static HTML, no JavaScript -->
<header>
<h1>My Blog</h1>
<nav>...navigation...</nav>
</header>
<main>
<!-- List of posts - static -->
{data.posts.map(post => (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
<!-- Interactive island -->
<SearchWidget client:load />
</main>
<!-- Static footer -->
<footer>© 2024</footer>
2. Island Hydration
// Each island hydrates independently
// With its own JavaScript bundle
<script type="module">
// Only loads for SearchWidget
import { hydrate } from 'framework';
import SearchWidget from './SearchWidget.js';
hydrate(SearchWidget, document.querySelector('[data-island="search"]'));
</script>
Frameworks Supporting Islands
Astro
---
// Server-side only
const posts = await Astro.glob('./posts/*.md');
---
<layout>
<!-- Static -->
<h1>{posts.length} Posts</h1>
<!-- Islands with different hydration strategies -->
<Counter client:load /> <!-- Immediate -->
<Comments client:idle /> <!-- When browser idle -->
<ImageGallery client:visible /> <!-- When scrolled into view -->
<ThemeToggle client:media="(max-width: 768px)" />
</layout>
11ty + Preact
// .eleventy.js
const preactRenderToString = require('preact-render-to-string');
module.exports = function(eleventyConfig) {
eleventyConfig.addShortcode('island', (component, props) => {
// Render on server
const html = preactRenderToString(h(component, props));
// Mark for client hydration
return `<div data-island="${component.name}" props='${JSON.stringify(props)}'>
${html}
</div>`;
});
};
Fresh (Deno)
// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
import Counter from "../islands/Counter.tsx";
export const handler: Handlers = {
async GET(req, ctx) {
const data = await fetchData();
return ctx.render(data);
},
};
export default function Page({ data }: PageProps) {
return (
<div>
<!-- Static -->
<h1>{data.title}</h1>
<p>{data.content}</p>
<!-- Island -->
<Counter start={data.count} />
</div>
);
}
Hydration Strategies
client:load
<!-- Hydrate immediately on page load -->
<ShoppingCart client:load />
client:idle
<!-- Hydrate when browser is idle -->
<SocialShare client:idle />
client:visible
<!-- Hydrate when scrolled into viewport -->
<ImageCarousel client:visible />
client:media
<!-- Hydrate based on media query -->
<MobileMenu client:media="(max-width: 768px)" />
client:only
<!-- Skip SSR, render only on client -->
<HeavyChart client:only="react" />
Benefits
1. Zero JavaScript by Default
Page: Blog post
Static HTML: 15KB
Islands: 2 (Search, Comments)
Island JS: 8KB
Total JS: 8KB vs 150KB (traditional SPA)
2. Independent Hydration
// Each island hydrates separately
// Failure in one doesn't break others
<!-- Even if Search fails -->
<SearchIsland />
<!-- Comments still work -->
<CommentsIsland />
3. Progressive Enhancement
<!-- Works without JavaScript -->
<form action="/search" method="GET">
<input type="search" name="q" />
<button type="submit">Search</button>
</form>
<!-- Enhanced with JavaScript -->
<SearchIsland client:load>
<!-- Falls back to form above -->
</SearchIsland>
Use Cases
Content Sites
---
// Blog, documentation, marketing
const articles = await getArticles();
---
<article>
<!-- Static content -->
<h1>{article.title}</h1>
<div class="content">
<Fragment set:html={article.content} />
</div>
<!-- Interactive islands -->
<div class="engagement">
<LikeButton client:load articleId={article.id} />
<ShareMenu client:load />
</div>
<CommentsSection
client:visible
articleId={article.id}
/>
</article>
E-commerce
---
const product = await getProduct(params.id);
---
<div class="product">
<!-- Static -->
<img src={product.image} alt={product.name} />
<h1>{product.name}</h1>
<p>{product.description}</p>
<!-- Interactive -->
<AddToCart
client:load
product={product}
/>
<ColorPicker
client:load
colors={product.colors}
/>
<Reviews
client:visible
productId={product.id}
/>
</div>
Best Practices
1. Keep Islands Small
<!-- Good: Focused island -->
<LikeButton client:load />
<!-- Bad: Too much in one island -->
<WholePage client:load />
2. Choose Right Hydration
<!-- Above fold, critical -->
<Navigation client:load />
<!-- Below fold, can wait -->
<FooterComments client:visible />
<!-- Enhancement, not critical -->
<Confetti client:idle />
3. Lazy Load Islands
<script>
// Load island only when needed
let islandLoaded = false;
document.getElementById('load-chart').addEventListener('click', async () => {
if (!islandLoaded) {
await import('./ChartIsland.js');
islandLoaded = true;
}
});
</script>
Key Takeaway
Islands Architecture delivers the performance of static sites with the interactivity of SPAs. Static by default, interactive on demand. Each island hydrates independently with optimal timing. Perfect for content-heavy sites where most of the page doesn’t need JavaScript.