Astro Islands: Partial Hydration That Ships Less JavaScript
Astro's island architecture hydrates only the interactive components you explicitly mark, shipping zero framework JavaScript for static content. This guide explains the mechanism, directive choices, composition patterns, and common pitfalls with a worked example.
05 Oct 2025, 01:09 UTC

The Problem: Full Hydration Waste
Traditional single-page applications hydrate the entire page, downloading and executing framework runtimes for every component — even static content like headers, footers, and markdown articles. Astro's island architecture solves this by rendering everything to static HTML at build time, then hydrating only the interactive components you explicitly mark. The result: most pages ship zero framework JavaScript.
How Islands Work
An island is a component subtree that hydrates independently. You create one by adding a client:* directive to a component tag in an .astro file. Astro injects a tiny hydration script for that specific subtree; the rest of the page remains plain HTML with no JavaScript payload.
Each island loads its own framework runtime (React, Vue, Svelte, Solid, Preact, or vanilla JS) only when its directive condition is met. Islands hydrate in parallel — order is not guaranteed — so they must not depend on each other's initialization.
Worked Example: A Static Blog with an Interactive Search Island
Create a new Astro project and add the React integration:
npm create astro@latest my-blog
cd my-blog
npx astro add reactCreate a static layout component (src/layouts/PostLayout.astro) that renders markdown content with no interactivity:
--- const { title } = Astro.props; ---
{title}
{title}
© 2026
Create an interactive search component (src/components/Search.jsx) using React:
import { useState } from 'react';
export default function Search({ posts }) {
const [query, setQuery] = useState('');
const filtered = posts.filter(p =>
p.title.toLowerCase().includes(query.toLowerCase())
);
return (
setQuery(e.target.value)}
/>
{filtered.map(p => (
- {p.title}
))}
);
}In a page (src/pages/index.astro), render the static layout and embed the search island with client:visible so its JavaScript loads only when the user scrolls it into view:
--- import PostLayout from '../layouts/PostLayout.astro';
import Search from '../components/Search.jsx';
const posts = await Astro.glob('../content/posts/*.md'); ---
Latest Posts
{posts.map(post => (
- {post.frontmatter.title}
))}
({
slug: p.frontmatter.slug,
title: p.frontmatter.title,
url: p.url
}))} />
Build and inspect dist/ — you'll see the page HTML contains the rendered post list and a placeholder <div data-astro-cid-...></div> for the search island, plus a small inline script that registers the island. The React bundle loads only when the search component enters the viewport.
Directive Options: Choose the Right Trigger
| Directive | When Hydration Starts | Typical Use Case |
|---|---|---|
client:load | Immediately on page load | Critical UI (navigation, cart) that must be interactive instantly |
client:idle | After requestIdleCallback fires | Non-critical widgets (chat, recommendations) |
client:visible | When element enters viewport (IntersectionObserver) | Below-fold content (search, comments, carousels) |
client:media | When a media query matches | Responsive islands (mobile-only drawer, desktop sidebar) |
client:only | Client-only; no server render | Components needing window, localStorage, or broken SSR libraries |
Practical check: Open DevTools Network tab, load a page with client:visible, and verify the island's JS chunk appears only after scrolling the element into view. A page with only static components should show zero framework JS.
Composition Patterns and Nesting
Islands can nest: a parent hydrated with client:load can contain child components from the same framework that share the hydration boundary. This avoids duplicate runtime loads.
Mixing frameworks across island boundaries works — a React island and a Vue island can coexist — but each adds its own runtime. A page with React, Vue, and Svelte islands loads three framework bundles. Prefer a single framework per project unless you're migrating incrementally.
Astro 4.x+ supports inline client logic without a separate component file using <script type="module" hoist> in .astro files. The define:vars directive passes server-side values to client scripts as JSON-encoded props:
<script type="module" hoist>
export function init(count) { console.log(count); }
</script>
<div data-count={42} define:vars={{ count: 42 }}>
<script type="module">
import { init } from './init.js';
init(JSON.parse(document.currentScript.previousElementSibling.dataset.count));
</script>
</div>Limits and Common Mistakes
Hydration Order Is Not Guaranteed
Separate islands hydrate in parallel. Do not rely on one island's JavaScript executing before another's. For coordination, use shared state (nanostores, signals, or URL search params) rather than timing assumptions.
Framework Runtime Overhead
Each distinct framework used in islands adds its runtime. Measure total JS payload with Lighthouse or WebPageTest when mixing frameworks.
client:visible Edge Cases
IntersectionObserver may not trigger if the element is hidden via display: none or inside a closed shadow DOM. Test with real viewport conditions — not just desktop resize.
client:only Flash of Missing Content
client:only components receive no server-rendered HTML, causing a visible flash on slow connections. Provide a fallback skeleton or use client:load with a loading state instead.
Large Props Inflate HTML
Passing large data structures as props serializes them into the HTML as JSON, increasing page weight. For large datasets, fetch data client-side inside the island or use Astro's server endpoints.
Middleware Cannot Access Island State
Middleware runs before island hydration and cannot directly access island state. Communicate via cookies, headers, or URL search params.
Verification Checklist
- Create a test project with
npm create astro@latestand add one framework integration. - Build with
npm run buildand inspectdist/— confirm only island components have associated JS chunks. - Use DevTools Network tab to verify
client:visibleloads JS on scroll, and static-only pages load zero framework JS. - If mixing frameworks, add a second integration (e.g.,
@astrojs/vue), create islands from both, and measure total payload. - For
viewTransitions(Astro 3.0+), enable inastro.config.mjs, add<ViewTransitions />to a layout, and navigate between pages sharing an island with internal state (e.g., a counter) to verify state persistence.
These steps confirm the architecture behaves as documented for your Astro version. Always check the current directives reference at docs.astro.build for version-specific behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.