← All posts

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:idle directives 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.