Partial hydration & islands architecture 101
Modern JavaScript frameworks ship a lot of JavaScript. But most websites don’t need a lot of JavaScript.
On one end of the spectrum, you have dashboards, SaaS apps, and real-time editors — deeply interactive pages where JS is the product. On the other end, documentation, blogs, and marketing pages that are pure content and need zero JavaScript.
But there’s a whole category of sites in between that need some interactivity, just not a lot:
These “goldilocks” sites are awkward for traditional frameworks. You can’t fully statically generate them because some parts need to be interactive, but shipping an entire framework runtime for a theme toggle and a few animated icons feels like overkill.
The root of the problem is hydration. When a server-rendered page reaches the browser, the framework loads and hydrates the entire component tree — walking every node, attaching every listener — even for the 90% of the page that’s static content and will never respond to a click. That’s a lot of JavaScript downloaded and executed for nothing.
Partial hydration flips the model. Instead of hydrating everything and carving out static parts, you start from zero JavaScript and opt in to interactivity only where you need it. The rest of the page stays as pure, server-rendered HTML with no runtime overhead.
This is the idea behind the islands architecture, a term coined by Etsy’s frontend architect Katie Sylor-Miller and later popularised by Preact creator Jason Miller. Think of your page as an ocean of static HTML with small, independent “islands” of interactivity scattered across it. Each island hydrates on its own, in isolation, without knowing about or depending on the others.
How Astro implements it
Astro is probably the most well-known framework built around this model. By default, Astro components render to static HTML at build time with zero client-side JavaScript. When you need interactivity, you explicitly opt in using client:* directives on a per-component basis:
---
// Everything here runs at build time only
import Header from '../components/Header.astro' // static, no JS
import SocialLinks from '../components/SocialLinks' // React component
import GitHubCalendar from '../components/GitHubCalendar' // React component
---
<!-- Pure HTML, zero JS -->
<Header />
<!-- These become interactive islands -->
<SocialLinks client:idle />
<GitHubCalendar client:only="react" />The client:* directives control when each island hydrates:
| Directive | When it hydrates | Good for |
|---|---|---|
client:load | Immediately on page load | Critical interactive UI that must work right away |
client:idle | After the page is done loading (browser idle) | Non-critical interactive components like theme toggles, animated icons |
client:visible | When the component scrolls into view | Below-the-fold content like pagination arrows, newsletter forms |
client:only="react" | Client-side only, never SSR’d | Components that depend entirely on browser APIs (e.g. GitHub contribution calendar) |
client:media="(max-width: 768px)" | When a CSS media query matches | Mobile-only interactive components |
How this site uses it
This site you’re reading is built with Astro. The vast majority of it — the sidebar, the blog content, the navigation, all the layout — renders to static HTML at build time. No framework, no virtual DOM, no JavaScript. It’s just HTML and CSS.
The interactive islands are small and deliberate:
- Animated icons (
client:idle) — the little SVG icons that animate on hover throughout the site. They hydrate after the page settles so they don’t compete with the initial render. - Theme and notification toggles (
client:idle) — the brightness/moon icons and bell icons in the header. Interactive but not urgent. - Social links (
client:idle) — React components that handle hover animations. - GitHub contribution calendar (
client:only="react") — fully client-rendered because it depends on browser APIs and fetches data at runtime. No server-side HTML is even attempted. - Pagination chevrons and newsletter form (
client:visible) — only hydrate when you scroll down to them. Why load JavaScript for something you might never see? - Vinyl collection grid (
client:idle) — the interactive filtering and sorting on the vinyls page.
Each of these islands loads and hydrates independently. If the GitHub calendar fails to load, the rest of the page is completely unaffected — it was never JavaScript to begin with.
Why it matters
The performance difference is real. A traditional React SPA would ship the framework plus every component’s JavaScript to the browser, even for a page that’s 95% static prose. With islands, that same page ships close to zero JS unless you explicitly need it.
This is particularly noticeable on content-heavy sites like blogs and documentation. The content is the product, and it doesn’t need reactivity. Islands architecture lets you get the DX of component-based frameworks (use React, Vue, Svelte — Astro doesn’t care) without paying the runtime cost for static content.
Islands architecture and partial hydration are closely related but not identical. Partial hydration is the technique (hydrate only some components). Islands architecture is the mental model (static ocean, interactive islands).
Astro uses partial hydration to implement the islands model. Other approaches to partial hydration exist too — React Server Components, for example, solve a similar problem but from within a single-framework paradigm rather than Astro’s framework-agnostic approach.
