Skip to content
Astro interview 4 min read

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 .astro file with no client:* 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.

DirectiveWhen it hydratesBest for
client:loadImmediately on page loadCritical, above-the-fold interactivity
client:idleWhen the browser is idle (requestIdleCallback)Low-priority widgets that can wait
client:visibleWhen the element enters the viewportBelow-the-fold components, long pages
client:media={query}When a CSS media query matchesMobile-only menus, responsive widgets
client:only={framework}Skips SSR entirely; renders only on clientComponents 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:load for above-the-fold, must-be-instant interactivity; prefer client:visible or client:idle everywhere else.
  • Use client:only sparingly — it sacrifices SSR and SEO and risks layout shift; always pair it with the framework name.
  • Reach for server:defer server islands instead of client islands when the dynamic content needs no interactivity, just fresh server data.
  • Keep islands small and focused; a giant client:load component defeats the architecture’s purpose.
  • Use nanostores (not React Context) to share state across islands, since each island is an isolated runtime.
Last updated June 14, 2026
Was this helpful?