Astro Islands: Shipping JavaScript Only Where Your Page Actually Needs It
Astro ships zero JavaScript by default and hydrates only the components you mark with a client directive. Here's how islands work, which directive to pick, and where the model stops paying off.
10 Feb 2026, 11:06 UTC

You've built a mostly-static marketing site, but the Lighthouse report says you're shipping 300 KB of JavaScript. The culprit: your framework hydrated the entire page — header, footer, hero text, everything — just so one search box and a carousel could be interactive. Astro's island architecture exists precisely to fix this mismatch.
The short version: Astro renders every component to plain HTML at build time and sends zero JavaScript by default. You opt individual components into interactivity with a client directive, and only those components ship JS. This post explains how that works, when each directive makes sense, and where the model breaks down.
What an island actually is
In most single-page-app frameworks, hydration is all-or-nothing: the browser downloads one bundle and re-creates the whole component tree in memory before anything responds to input. Astro flips that default. Each interactive component is an independent "island" — a self-contained unit that loads and hydrates on its own schedule, surrounded by static HTML that costs nothing at runtime.
Because islands are isolated, they don't share a runtime unless you make them. A React search box and a Vue carousel can coexist on the same page, each with its own small bundle, neither aware of the other. State libraries can be scoped to a single island, which also makes interactive widgets easier to test in isolation.
Choosing a client directive
A component only hydrates if you attach a directive. Picking the right one is the main engineering decision:
client:load— hydrate immediately on page load. Use for things the user needs instantly, like a nav menu.client:idle— hydrate when the main thread is free (viarequestIdleCallback). Good for below-the-fold widgets that shouldn't block first paint.client:visible— hydrate when the element scrolls into view. Best for carousels, comment sections, anything far down the page.client:media— hydrate only when a CSS media query matches, e.g. a mobile-only menu:client:media="(max-width: 768px)".
A worked example: a counter that hydrates on idle
Assume Astro 4.x. In a fresh project (npm create astro@latest), create a small React counter and use it from an Astro page:
// src/components/Counter.jsx
import { useState } from 'react';
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Count: {n}</button>;
}---
// src/pages/index.astro
import Counter from '../components/Counter.jsx';
---
<html><body>
<h1>Static heading — no JS shipped for this</h1>
<Counter client:idle />
</body></html>Run npm run dev, then open DevTools' Network tab and reload. The initial HTML already contains the counter's button markup (server-rendered), but no counter script arrives with it — a separate chunk loads after the idle callback fires. Remove client:idle and rebuild: the button still renders, but no JavaScript is emitted at all, and clicking does nothing. That toggle is the whole mental model.
To verify a production build, run npm run build && npm run preview and check the generated HTML in dist/: the counter markup is present as static HTML, and the directive's script tag is the only JS associated with it.
The trade-off: islands are cheap, not free
Every hydrated island carries its own bundle and hydration cost. Sprinkle client:load on fifteen components and you've rebuilt the SPA you were trying to escape — except now with fifteen separate bundles instead of one shared one. The performance win comes from restraint: hydrate the two components that need it, leave the other fifty static.
Two other limitations worth knowing:
- Cross-island communication requires explicit plumbing (custom events or a shared store like nanostores). If your page is one giant interactive app where everything talks to everything, islands add friction rather than removing weight.
- Islands that depend on server-only APIs need a server rendering target; a purely static
astro buildcan't render them. Check your adapter setup before committing to a component that reads cookies or hits a database at render time.
A practical rule of thumb
Audit one page: view-source it and count how much of the markup is genuinely interactive. If it's under 20%, Astro's defaults will likely cut your JS payload dramatically — but only if you reach for client:visible and client:idle before client:load. Start with the laziest directive that still feels instant to the user, verify in the Network tab that the chunk loads when you expect, and only escalate to earlier hydration when you can point at a real interaction delay.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.