Astro Islands Architecture: When SSR Meets Frontend Frameworks
A deep dive into Astro's Islands architecture — start with zero JavaScript and hydrate only the interactive components on demand, balancing first-paint performance with developer experience.
Summary: Islands architecture is “static by default, interactive on demand”. Starting from a browser performance budget, this post explains why whole-page hydration is wasteful and how Astro’s
client:load,client:idledirectives control exactly when scripts load.
Where the problem starts: interaction is expensive
Every modern frontend framework does the same thing: render a component tree into the DOM, then hydrate — re-execute component logic in the browser and bind events. For a content-heavy page, that means downloading and executing a few hundred KB of framework runtime just for one or two buttons.
Whole-page hydration: framework runtime (≈100KB) + page components (≈60KB) + router (≈30KB)
Islands: only interactive components load scripts
What Islands are
Islands architecture (introduced by Etsy’s Katie Sylor-Miller, systematized by Preact author Jason Miller) treats a page as “interactive islands in a static ocean”:
- Static regions: HTML + CSS, zero JavaScript
- Interactive islands: independent components that each load their own scripts
// An island: only this component hydrates
import Counter from '../components/Counter.tsx';
<Counter client:load />;
In practice with Astro
Astro provides five client: directives to control when hydration happens:
| Directive | When | Use case |
|---|---|---|
client:load |
Immediately after page load | Above-the-fold critical interaction |
client:idle |
When the browser is idle | Non-critical components |
client:visible |
When the element enters the viewport | Collapsed regions |
client:media |
When a media query matches | Mobile-only menus |
client:only |
Client-side only | Components that depend on browser APIs |
---
import ThemeToggle from '../components/ThemeToggle.astro';
---
<!-- Loads only when the user approaches it -->
<ThemeToggle client:visible />
Islands don’t share runtime state with each other and carry no “hydration data” burden from the server — that’s the key difference from whole-page SSR in frameworks like Next.js.
Data flow: keep state in the HTML
Under Islands, server-to-client data transfer happens through prop serialization:
// Props are rendered server-side and reused during hydration
<SearchBox
initialQuery={query}
results={results.map((r) => ({ id: r.id, title: r.title }))}
client:idle
/>
Astro serializes the props into a <script type="application/json"> that hydration reads — no second request, and the initial HTML stays complete, indexable, and flicker-free.
When not to use Islands
Islands are not a silver bullet. Consider other approaches when:
- The whole page is highly interactive (e.g. an online document editor) — an SPA or whole-page hydration fits better
- You need shared global state across many components — the isolation model makes state passing tedious
- SEO doesn’t matter (internal tools) — you can skip static rendering entirely
Wrap-up
- For content-first pages, stay static by default and hydrate components on demand with
client:directives - Push non-critical scripts later with
client:visible/client:idle - Pass initial state through prop serialization to keep the HTML complete
The payoff is direct: first-paint JavaScript can drop to a few KB, and Lighthouse performance scores comfortably exceed 95. Next post, we’ll build type-safe content collections on top of this architecture.