Type something to search...
How does Astro's island architecture work?

How does Astro's island architecture work?

Astro's island architecture works by rendering the whole page to static HTML on the server, then making only selected components interactive in the browser. Each interactive component, called a client island, is marked with a client:* directive. Astro wraps it in an <astro-island> custom element that loads that component's JavaScript on its own schedule and hydrates it in isolation from everything else. Astro also has server islands: components marked with server:defer that are left out of the initial HTML and rendered separately on the server, so slow or personalised content doesn't hold up the rest of the page.

The result is a page that's mostly lightweight HTML with a few self-contained "islands" of interactivity or dynamic content. In this article you'll learn where the idea came from, how client islands are built and hydrated, what each client directive does, how props and children are passed into islands, how server islands work under the hood, and the limits you'll run into in practice. All the output shown was produced with Astro 7.4.2.

Where the Idea Came From

The term "component island" was coined by Etsy's frontend architect Katie Sylor-Miller in 2019, and Preact's creator Jason Miller expanded on it in a 2020 article. His summary is still the best one-line definition: render HTML pages on the server and inject placeholders around highly dynamic regions that can then be hydrated on the client into small self-contained widgets.

The technique is also called partial or selective hydration. It contrasts with the single-page application (SPA) model, where the whole page is one JavaScript application that must download and hydrate before any of it is interactive. Astro was the first mainstream framework to build selective hydration in as the default.

Static by Default

The starting point matters. In Astro, every component renders to HTML and its JavaScript is discarded unless you say otherwise. That applies to .astro components, which have no client runtime at all, and to framework components from React, Vue, Svelte, Solid or Preact.

Take this React counter:

// src/components/Counter.jsx
import { useState } from 'react';

export default function Counter({ start = 0, label = 'Count' }) {
  const [count, setCount] = useState(start);
  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      {label}: {count}
    </button>
  );
}

Used as <Counter label="Static" /> in an Astro page, it renders to this and nothing more:

<button type="button">Static<!-- -->: <!-- -->0</button>

The button looks right but does nothing when clicked, because no React code reached the browser. That's deliberate: plenty of components (a card, a formatted date, a syntax-highlighted code block) only need to render once.

Turning a Component Into a Client Island

To make the counter interactive, add a client directive:

---
// src/pages/islands.astro
import Base from '../layouts/Base.astro';
import Counter from '../components/Counter.jsx';
---
<Base title="Islands">
  <h1>Product page</h1>
  <p>This paragraph is plain HTML.</p>
  <Counter label="Static" />
  <Counter label="Load" client:load />
  <Counter label="Idle" client:idle />
  <Counter label="Visible" client:visible />
  <Counter label="Mobile" client:media="(max-width: 50em)" />
  <Counter label="Client only" client:only="react" />
</Base>

What Ends Up in the HTML

Building this page shows exactly what an island is. Here's the client:load counter from the output, with line breaks added:

<astro-island uid="Zd7fOO" prefix="r6"
  component-url="/_astro/Counter.B_ml4rn-.js"
  component-export="default"
  renderer-url="/_astro/client.Cd-EeyjT.js"
  props="{&quot;label&quot;:[0,&quot;Load&quot;]}"
  ssr client="load"
  opts="{&quot;name&quot;:&quot;Counter&quot;,&quot;value&quot;:true}"
  await-children>
  <button type="button">Load<!-- -->: <!-- -->0</button><!--astro:end-->
</astro-island>

Each attribute has a job:

  • component-url: the bundled JavaScript for this one component.
  • renderer-url: the framework's client renderer, here React DOM. It's shared, so the browser downloads it once however many React islands the page has.
  • props: the component's props, serialised so they can be passed to the component again in the browser.
  • client: which directive to apply, and therefore when to load.
  • ssr: the island already contains server-rendered HTML, so the visitor sees the button immediately and hydration just attaches behaviour to it.

A small inline script defines the astro-island custom element once per page, and a tiny loader script for each directive type decides when to trigger it. When it fires, the element imports the component and renderer, deserialises the props and hydrates the existing markup.

The page also gets one line of CSS, astro-island,astro-slot,astro-static-slot{display:contents}, so the wrapper element doesn't affect your layout.

Islands Are Independent

Every island hydrates on its own. A slow carousel low on the page doesn't block a header menu at the top. You can even use islands from different frameworks on one page, as long as each framework's integration is installed. If two islands use the same framework, its runtime is only sent once.

The cost of each island is roughly its framework runtime plus its own code. In this build, the React renderer was about 66 KB gzipped and the Counter component itself under 1 KB, which is why lighter frameworks or a plain <script> are worth considering for small widgets.

Choosing the Right Client Directive

The directive controls when the JavaScript loads, which is often more important than how much there is:

DirectiveLoads whenGood for
client:loadImmediately on page loadAbove-the-fold controls that must work at once, such as a buy button
client:idleThe browser is idle (requestIdleCallback, falling back to load)Lower-priority UI, such as a newsletter form
client:idle={{ timeout: 500 }}Idle, or after the timeout in millisecondsLow priority but must be ready soon
client:visibleThe component enters the viewport (IntersectionObserver)Comments, carousels, anything below the fold
client:visible={{ rootMargin: "200px" }}The component comes within 200px of the viewportBelow-the-fold widgets that should be ready on arrival
client:media="(max-width: 50em)"A CSS media query matchesA mobile menu toggle
client:only="react"Immediately, rendering in the browser onlyComponents that can't run on the server

client:only is the odd one out. It skips server rendering, so the island in the output is empty:

<astro-island uid="ZgLWvO" component-url="/_astro/Counter.B_ml4rn-.js" component-export="default" renderer-url="/_astro/client.Cd-EeyjT.js" props="{&quot;label&quot;:[0,&quot;Client only&quot;]}" ssr client="only" opts="{&quot;name&quot;:&quot;Counter&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island>

You must pass the framework name ("react", "preact", "svelte", "vue" or "solid-js") because Astro never runs the component and can't work it out. You can show placeholder content until it loads with a child that has slot="fallback". There's more on this in how to use client:only in Astro, and the full directive reference is in Astro client directives explained.

Passing Data Into Islands

Props Must Be Serialisable

Because props are written into the HTML and read back in the browser, they must be serialisable. Astro supports plain objects, number, string, Array, Map, Set, RegExp, Date, BigInt, URL, Uint8Array, Uint16Array, Uint32Array and Infinity.

Functions are the common trap. This won't work:

<SearchBox client:load onSearch={(q) => console.log(q)} />

The function exists only on the server and can't be sent to the browser. Keep event handling inside the island, or communicate between islands with a shared store.

Remember too that everything you pass as a prop is visible in the page source. Don't pass a full user record when the island only needs a display name.

Children and Named Slots

You can pass children into a framework island from an Astro file. React, Preact and Solid receive them as children; Svelte and Vue receive them through <slot />:

---
// src/pages/sidebar.astro
import MySidebar from '../components/MySidebar.jsx';
---
<MySidebar client:visible>
  <h2 slot="title">Menu</h2>
  <p>Here is a sidebar with some text.</p>
</MySidebar>

For React, Preact and Solid, the named slot becomes a prop (props.title), with kebab-case names converted to camelCase. Children are rendered on the server, so you can even pass static .astro components into an island this way. What you can't do is import an .astro component inside a .jsx or .svelte file, or put a client:* directive on an Astro component: Astro components have no browser runtime to hydrate.

Sharing State Between Islands

Islands are isolated, but they can still communicate. Because each one is a separate application, React context won't cross the boundary. The usual solution is a small framework-agnostic store, such as Nano Stores, that each island imports. Custom browser events and URL state also work. See how to share state between Astro islands for worked examples.

Server Islands

Client islands solve interactivity. Server islands solve a different problem: a page that's mostly the same for everyone but has a few personalised or slow parts.

Deferring a Component

Server islands need an adapter, because something has to render the deferred component at request time. With @astrojs/node installed, mark the component with server:defer:

---
// src/components/Greeting.astro
const name = Astro.cookies.get('name')?.value ?? 'guest';
const time = new Date().toISOString();
---
<p>Welcome back, {name}. Rendered at {time}.</p>
---
// src/pages/server-island.astro
import Base from '../layouts/Base.astro';
import Greeting from '../components/Greeting.astro';
---
<Base title="Server island">
  <h1>Our shop</h1>
  <Greeting server:defer>
    <p slot="fallback">Loading your greeting...</p>
  </Greeting>
</Base>

The page itself is still prerendered at build time. The greeting reads a cookie, which is only possible at request time, so it's deferred.

How Server Islands Work

In the built page, the Greeting component is gone. In its place are the fallback content and a small script:

<p>Loading your greeting...</p>
<script type="module" data-astro-rerun data-island-id="0227ac38-c0c8-4dac-b51d-1348fce42e64">const headers = new Headers({});
let response = await fetch("/_server-islands/Greeting?e=6FBD...&p=&s=", { headers });replaceServerIsland("0227ac38-c0c8-4dac-b51d-1348fce42e64", response);</script>

Astro also adds a <link rel="preload" as="fetch"> for that URL in the <head>, so the request starts early. Each deferred component becomes its own route under /_server-islands/. Requesting it with a cookie returns just the component's HTML:

curl -H 'Cookie: name=Sajjad' "http://localhost:4567/_server-islands/Greeting?e=6FBD...&p=&s="
<p>Welcome back, Sajjad. Rendered at 2026-10-24T05:01:14.151Z.</p>

A few details from the docs are worth knowing:

  • Props are encrypted in the query string. The key is generated per build; for rolling or multi-region deployments, generate a stable one with astro create-key and set it as the ASTRO_KEY environment variable.
  • Islands load via GET, so you can cache them with normal Cache-Control headers. If the props make the URL longer than about 2048 bytes, Astro switches to a POST request, which browsers won't cache. Pass only the props you need.
  • Astro.url isn't the page URL inside a server island. It looks like /_server-islands/Greeting. Read the Referer header if you need the page's address or query string.
  • Each island loads independently, so one slow island doesn't delay the others.

Server island props follow the same serialisation rules as client islands, and functions can't be passed. Read how to use server islands in Astro for caching and fallback patterns.

Client Islands vs Server Islands

Client islandServer island
Marked withclient:load, client:visible, etc.server:defer
Runs inThe browser (after server rendering, unless client:only)The server, at request time
Works onFramework components (React, Vue, Svelte...)Astro components
Needs an adapterNoYes
Sends JavaScriptThe component plus its framework runtimeA small fetch-and-replace script
Typical useMenus, carousels, forms, searchAvatars, basket counts, prices, recommendations

The two combine well. A product page can be fully prerendered and cached at the edge, with a server island for the visitor's basket and a client island for the image gallery.


FAQ: Astro Islands

An island is a component that's handled separately from the static HTML around it. A client island is hydrated in the browser with its own JavaScript, and a server island is rendered on the server after the main page and swapped in when ready.

No. Astro components have no client-side runtime, so adding a client directive to one causes an error. Use a framework component for the island, or add a script tag inside the Astro component for small amounts of interactivity.

No. The framework renderer is a shared file, so it's downloaded once per page however many islands use it. Each island adds only its own component code on top.

Island props are serialised into the HTML so they can be read again in the browser, and functions can't be serialised. Keep handlers inside the component, or use a shared store or custom events to communicate between islands.

The page can be static, but you need an adapter installed so something can render the deferred component on request. Without an adapter, server:defer has nothing to fetch from.

Use client:visible for anything below the fold and client:idle for low-priority UI near the top. Save client:load for controls that must work the instant the page appears.


Conclusion

Astro's island architecture starts from static HTML and adds work only where it's needed. A client directive turns a framework component into an <astro-island> that loads its own code at a time you choose, hydrates in isolation and shares its framework runtime with other islands. A server:defer directive takes a slow or personalised component out of the main render and fetches it from its own encrypted route once the page has loaded.

In practice, most of your components should have no directive at all. Pick the latest sensible loading strategy for the ones that do, keep their props small and serialisable, and use server islands to keep cached pages fast while still showing personalised content.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper