Skip to main content

Islands Architecture: Complete Guide to Partial Hydration 2026

Created: March 7, 2026 CalmOps 6 min read

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:

  1. Download all JavaScript for the page
  2. Parse and compile the JavaScript
  3. Execute JavaScript to attach event listeners
  4. 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

  1. Default to Static: The page is primarily static HTML
  2. Islands of Interactivity: Only interactive components include JavaScript
  3. Independent Hydration: Each island hydrates independently
  4. 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:

  1. Renders static content immediately (no JavaScript needed)
  2. Identifies islands by their markers
  3. Downloads JavaScript only for each island
  4. 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

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

Share this article

Scan to read on mobile