Islands & Hydration Questions
Islands architecture is the headline feature interviewers probe when they want to know whether you actually understand why Astro ships zero JavaScript by default. The questions below move from “explain the model” to subtle trade-offs around client:* directives, server islands, and the performance cost of hydration. Strong answers connect the directive you pick to a concrete network and interactivity outcome.
What is islands architecture, and how does Astro implement it?
Islands architecture means a page is mostly static, server-rendered HTML with small, isolated regions of interactivity (“islands”) that hydrate independently. Everything else stays as plain HTML with no JavaScript shipped at all.
In Astro this is the default: a .astro component renders to HTML on the server and ships zero client JS. You opt into interactivity per-component using a framework component (React, Vue, Svelte, Solid, Preact) plus a client:* directive. Each hydrated component is its own bundle and hydrates in isolation, so a heavy widget in the footer never blocks a button in the header.
---
import Counter from "../components/Counter.jsx";
import Newsletter from "../components/Newsletter.svelte";
---
<h1>Static heading — no JS</h1>
<Counter client:load /> {/* island 1 */}
<Newsletter client:visible /> {/* island 2 */}
A common follow-up: “What runs on the client in a
.astrofile with noclient:*directive?” Answer: nothing. The---script runs only at build/request time on the server. A plain<script>tag is the only client JS, and it runs once globally, not as a hydrated island.
Walk me through the client directives
Each directive controls when the JavaScript for an island loads and hydrates. Picking the right one is the single biggest performance lever in Astro.
| Directive | When it hydrates | Best for |
|---|---|---|
client:load | Immediately on page load | Critical, above-the-fold interactivity |
client:idle | When the browser is idle (requestIdleCallback) | Low-priority widgets that can wait |
client:visible | When the element enters the viewport | Below-the-fold components, long pages |
client:media={query} | When a CSS media query matches | Mobile-only menus, responsive widgets |
client:only={framework} | Skips SSR entirely; renders only on client | Components that can’t run on the server |
---
import Carousel from "../components/Carousel.jsx";
---
<Carousel client:visible={{ rootMargin: "200px" }} />
<MobileNav client:media="(max-width: 768px)" />
The key distinction interviewers want: client:only does not server-render the component. You must name the framework (client:only="react") so Astro knows which runtime to load, and you accept a flash of empty space until the client renders it.
When would you choose client:visible over client:load?
Choose client:visible whenever a component is not immediately needed — anything below the fold, or any island whose interactivity the user won’t reach until they scroll. It defers both the download and the hydration cost using an IntersectionObserver, keeping the initial JS payload and main-thread work minimal.
Use client:load only for interactivity the user might touch in the first moment: a search box, a primary CTA, or a cart counter. Over-using client:load recreates the “hydrate everything” problem that islands were designed to avoid.
What is hydration and why is it expensive?
Hydration is the process of attaching client-side JavaScript (event listeners, reactive state) to already-rendered HTML. It’s expensive because the framework must re-run component code on the client, rebuild its virtual DOM or reactive graph, and reconcile it against the existing markup — all on the main thread, competing with user input.
Astro mitigates this three ways: partial hydration (only islands hydrate, not the page), independent hydration (islands don’t wait for each other), and lazy directives (idle/visible) that move the work off the critical path.
What are server islands?
Server islands (Astro 4.12+) let you defer rendering of a server component so the rest of the page can be cached or streamed immediately. You mark a component with server:defer and provide fallback content; Astro renders a placeholder, then fetches and slots in the real HTML via a separate request.
---
import Avatar from "../components/Avatar.astro";
---
<Avatar server:defer>
<div slot="fallback">Loading profile…</div>
</Avatar>
This is ideal for personalized or slow data (a logged-in user’s avatar, live pricing) on an otherwise static, CDN-cached page — the page ships instantly and the dynamic island fills in. It is server-side, so it ships no extra client JS, unlike a client:* island.
How do islands share state?
Components hydrated as separate islands are isolated — they don’t share a React tree or context, so you can’t pass state between them with props alone. The standard answers: use nanostores (framework-agnostic shared atoms) or browser primitives like localStorage / custom DOM events.
import { atom } from "nanostores";
export const cartCount = atom(0);
<Header client:load /> {/* reads cartCount */}
<AddToCart client:visible /> {/* writes cartCount */}
Both islands import the same store and stay in sync without a shared component boundary.
Best Practices
- Default to no directive — ship static HTML and only add
client:*where interactivity is genuinely required. - Reserve
client:loadfor above-the-fold, must-be-instant interactivity; preferclient:visibleorclient:idleeverywhere else. - Use
client:onlysparingly — it sacrifices SSR and SEO and risks layout shift; always pair it with the framework name. - Reach for
server:deferserver islands instead of client islands when the dynamic content needs no interactivity, just fresh server data. - Keep islands small and focused; a giant
client:loadcomponent defeats the architecture’s purpose. - Use nanostores (not React Context) to share state across islands, since each island is an isolated runtime.