Astro Islands: Shipping JavaScript Only Where It Is Needed
Astro’s Islands architecture lets you keep pages static and hydrate individual components with client directives. Learn how client:load, client:visible, and client:only work, with a practical example and common pitfalls.
26 Dec 2025, 19:59 UTC

The Practical Answer
Astro’s Islands architecture solves a specific problem: you want a mostly static page, but a few widgets need JavaScript. Instead of shipping one large bundle for the whole page, you mark individual components with a client:* directive. Astro renders the component to static HTML on the server, then loads a separate, small JavaScript chunk only for that component when the directive’s condition is met.
How Astro Islands Work
An “island” is an interactive UI component isolated from the surrounding static content. When you import a framework component—such as React, Preact, Svelte, Vue, or Solid—into an .astro page and add a directive like client:load, Astro performs two passes:
- Server render. Astro generates the component’s HTML and sends it in the initial document. Users see content immediately, even before JavaScript executes.
- Client hydration. After the page loads, Astro fetches the component’s JavaScript bundle and attaches event listeners to the existing DOM node. The markup does not change; only behavior is added.
Components without a directive remain pure HTML and CSS. Astro also scopes styles written inside the component, so CSS does not leak into other islands or the page layout.
A Worked Example: A Counter Widget
The following example uses Preact, but the pattern is identical for other supported frameworks. Assume Astro 4.x or 5.x with the Preact integration installed.
1. Create the component in src/components/Counter.tsx:
import { useState } from 'preact';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
);
}
2. Use it in a page, for example src/pages/index.astro:
---
import Counter from '../components/Counter.tsx';
const isBelowFold = true;
---
<html>
<body>
<h1>Dashboard</h1>
<p>Static content loads instantly.</p>
{isBelowFold && <Counter client:visible />}
</body>
</html>
Here, client:visible tells Astro to delay hydration until the button scrolls into the viewport. The HTML for the button is present on first paint, but the click handler is inert until the chunk loads.
Choosing the Right Directive
Astro provides several directives so you can trade off interactivity against payload size:
| Directive | Hydration trigger | Typical use |
|---|---|---|
client:load | Immediately after page load | Critical controls such as navigation toggles |
client:idle | Once the browser reaches idle | Non-urgent widgets like chat launchers |
client:visible | When the element enters the viewport | Below-the-fold carousels or counters |
client:media | When a CSS media query matches | Mobile-only navigation drawers |
client:only | Skips server HTML; renders only on client | Components that depend on window at mount |
client:only requires a framework hint, for example client:only="preact", because Astro cannot render the initial HTML without knowing which runtime to use.
Limits and Common Mistakes
Server-only code inside islands
Hydrated components run in the browser. They cannot import Node.js modules, query a database, or read the filesystem. Fetch data in the Astro frontmatter or an API endpoint, then pass it as props.
Hydration mismatches
If the server-rendered HTML differs from the first client render—for example, because the component reads Date.now() or Math.random() during render—React or Preact will warn and may discard the server markup. Keep initial renders deterministic.
State across navigation
When a user navigates with View Transitions, islands re-mount by default. Local state such as a counter value resets. To persist state, use a store library compatible with your framework, or lift state to a shared parent outside the island boundary.
Heavy dependencies
Adding a large charting or date library to a single island undermines the performance goal. Audit the bundle with npm run build and consider loading the dependency dynamically inside the component, or moving the feature to a dedicated route.
Over-islanding
Marking a large list or complex layout as an island forces the browser to hydrate hundreds of DOM nodes. Keep islands small and focused; render long lists as static HTML and add interactivity only to individual rows or controls.
How to Verify the Result
- Run
npm run buildand inspectdist/_astro/. Each island should appear as its own hashed JavaScript file, not merged into a single global bundle. - Open the built page in a browser with DevTools → Network filtered to JS. Confirm the island chunk loads according to its directive (immediately, on idle, or when scrolled into view).
- Temporarily disable JavaScript in the browser. The static HTML from the server render should still display the component’s content.
- Set the directive to
client:loadand interact with the widget. If the handler does not fire, check the console for hydration errors or missing integration configuration.
These checks confirm that Astro is shipping static HTML by default and adding JavaScript only where the directive allows it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.