Architecture Note: Using Astro Islands for Selective Hydration
An architecture note on Astro Islands: how to isolate interactive components, keep static HTML fast, and verify selective hydration in practice.
05 Jul 2026, 10:38 UTC

Problem
Modern static site generators excel at delivering fast, SEO‑friendly HTML, but interactive UI built with frameworks like React, Svelte or Vue typically forces the entire page to ship a large JavaScript bundle. When only a small portion of the page needs client‑side behavior, sending unused JavaScript wastes bandwidth and delays time‑to‑interactive.
Requirements
- Render the majority of a page as static HTML on the server (or at build time).
- Isolate interactive parts so they hydrate only when a defined condition is met.
- Pass data from the server to the island safely, without exposing secrets.
- Keep the build process simple and compatible with Astro’s file‑based routing.
- Allow verification that static content remains usable when JavaScript is disabled.
Smallest Suitable Design
The design leverages Astro’s built‑in client:* directives to create "islands". A page file (src/pages/index.astro) contains static markup and one or more island components wrapped with a directive that determines when hydration occurs.
Example: a visible counter island
---// src/pages/index.astro
import Counter from '../components/Counter.jsx';
---
<h1>Welcome to the demo site</h1>
<p>This paragraph is pure static HTML.</p>
<Counter client:visible />
The Counter component is a standard React file (src/components/Counter.jsx) that receives no props in this example. The client:visible directive tells Astro to:
- Render the island as a placeholder
<div>in the HTML. - Load the component’s JavaScript only when the placeholder enters the viewport.
- Hydrate the component on the client, attaching event listeners and enabling interactivity.
Build‑time props serialization
If the island needs data, pass it as a plain JSON‑serializable prop:
---// src/pages/product.astro
import ProductDetails from '../components/ProductDetails.svelte';
---
<h2>{product.title}</h2>
<ProductDetails
client:idle
{product}
/>
During astro build, Astro serializes product to JSON and injects it into the island’s client‑side bundle. The server‑rendered HTML contains only the placeholder and a script tag with a dynamic import that loads the island’s code when the idle trigger fires.
Trust / Data Boundaries
The static HTML produced by Astro is trusted because it originates from the source files and contains no executable user‑provided code. Props passed to islands cross the trust boundary only as serialized JSON; therefore:
- Never place API keys, tokens, or other secrets in island props – they become visible in the emitted HTML or the client bundle.
- Avoid passing non‑serializable values (class instances, functions, DOM nodes). Astro will reject them at build time, preventing runtime hydration errors.
Operational Checks
To confirm the design works as intended, perform the following checks after each build:
- Inspect static output: Run
astro build(requires read/write access to the project directory) and opendist/index.html. Verify that non‑island sections contain no<script>tags, while each island has a script tag with a dynamic import likeimport('./Chunk-xyz.js'). - Network trigger verification: In Chrome DevTools, enable network throttling, reload the page with JavaScript enabled, and filter by
JS. Confirm that the island’s chunk is not requested until the trigger condition (e.g., element becomes visible or the main thread is idle) occurs. - JavaScript‑disabled test: Disable JavaScript in the browser, reload the page, and ensure that all static content (headings, paragraphs, non‑island markup) appears correctly. Island placeholders should be present but non‑functional.
- Prop serialization validation: Intentionally pass a non‑serializable prop, such as a
Dateobject, to an island. The build should fail with an error similar to "Prop 'date' is not JSON serializable". This demonstrates the validation boundary.
Failure Modes
- Non‑serializable props: Cause build‑time errors; the page will not emit.
- Hydration mismatch: If the server‑rendered placeholder differs from the client‑rendered output (e.g., due to conditional rendering that depends on window size), React/Svelte/Vue will log a hydration warning and may produce UI glitches.
- Trigger not firing: Misconfigured directives (e.g.,
client:mediawith a media query that never matches) leave the island forever unhydrated, resulting in a static placeholder. - Large island bundle: An island that imports a heavy library can exceed the performance budget, negating the benefit of selective hydration.
Conditions That Would Change the Design
- Full SSR requirement: If SEO or legal mandates demand that interactive content be rendered on the server (e.g., searchable text that must appear in the initial HTML), islands would be removed and the entire page rendered via Astro’s server‑side rendering or an external SSR service.
- Limited runtime: Targets that do not support dynamic
import()(certain edge workers or older browsers) cannot load island chunks on demand; a fallback would be to ship all framework code upfront or avoid islands. - Unsupported framework: Introducing a frontend library without an Astro adapter would require a custom integration or abandoning islands for that component.
- Strict CSP: A content‑security‑policy that disallows inline scripts or dynamic imports could block island hydration; the design would shift to pre‑bundling all island code or using a server‑rendered fallback.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.