React

Island hydration strategies in Astro

The client directive is a budget choice: load, idle, visible, media or only decide when an island downloads its JavaScript and what the page costs.

Key points

  • The cheapest directive that still shows content in time is almost always the right one.
  • client:visible relies on IntersectionObserver: a hidden island never hydrates.
  • The number to measure is the island chunk plus its framework runtime, not the page weight.

In Astro the hydration strategy is a budget choice: take the cheapest directive that brings the interactive content in before the user touches it, and measure how much JavaScript it adds.

The cost of each directive

DirectiveJavaScriptWhat the visitor feels
client:loadDownloaded at onceThe widget answers the first click
client:idleAfter the first free momentShort delay, no blocked render
client:visibleWhen the island enters the viewportNo cost for anyone who does not scroll
client:mediaWhen the query matchesDesktop never pays for the mobile menu
client:onlyNo HTML from the serverNo content for crawlers

Directives take a value: client:idle with timeout caps the wait, and client:visible with rootMargin starts loading 200 pixels early, so the island is ready when it reaches the screen.

---
import Search from "../components/Search.jsx";
import Gallery from "../components/Gallery.jsx";
---


Measure on the build, not by eye

The build lists one file per island: watch the island chunk plus the runtime of its framework, not the total page weight.

npm run build
ls -lh dist/_astro/*.js

That total is not a cap for an Astro page: it is the cost of a fully hydrated application. The useful difference is between two builds of the same page, one with the island and one without, because the framework runtime is paid once, not per component.

When the island is not needed

A block that opens and closes is not an island: <details> does that with no JavaScript. A form that submits works with a form action. The island arrives when state is needed that the server cannot compute.

A server island, zero JavaScript

---
import UserGreeting from "../components/UserGreeting.astro";
---


  Hello, guest.

server:defer moves the island HTML into a separate request: the page arrives at once with the fallback and the content replaces it when ready. There is no hydration, so no runtime, but there is an extra request and a visible fallback that must be designed.

The ceiling to keep

A page with three islands of the same framework pays the runtime once but three components. When the sum exceeds a single widget, merge the islands, because the boundary costs more than the code.

The decision rule

Rule: start from client:visible; drop to client:idle when the widget must be ready before the user reaches it, such as a search in the header; keep client:load for the single control that sits in the first screen and takes an immediate click. client:only remains for code that cannot run on the server.

The boundary has a fixed price: two islands of the same framework download the runtime once and two components, but each adds an entry point and an execution order to verify. Above four islands on one page, ask whether the widget should be one.

The limitation to plan for

client:visible relies on IntersectionObserver: an island inside a display: none container or a closed tab never enters the viewport and never hydrates. Use client:load, or hydrate on the first click on that tab.

Mistakes you will see

  • Adding client:load to the island at the bottom of the page: you pay at once for a component three screens away.
  • Expecting client:visible to save anything above the fold: it enters the viewport immediately.
  • Counting on the HTML of a client:only island: the server does not render it and the crawler finds nothing.
  • Measuring page weight instead of the island chunk: the difference vanishes in the total.
  • Wrapping a component that only takes static props in an island: you pay the runtime for DOM the server can already write.

Islands are filled with ready components: saastro and Astro Association install as source, and the hero block stays static until it gets a directive. The list of directives is in interactive islands in Astro, the case where hydration never happens in missing client directive, the comparison in Astro vs Next.js.

Frequently asked questions

Which directive for a widget above the fold?

client:load. Above the fold the widget enters the viewport immediately, so client:visible saves nothing and adds an IntersectionObserver round before loading.

Does client:idle take a timeout?

Yes, passed as a value. Without it hydration waits for the first free moment, which on a heavy page can arrive late. A 500 millisecond timeout sets a predictable ceiling.

Why do two islands not share state?

Because each one is a separate application with its own component and its own state. Sharing data needs an external store, such as nanostores, subscribed by both islands.

Is the framework runtime duplicated?

No, shared framework code lands in a chunk common to islands of the same kind. The components stay separate, and those are the part you write.

Does client:visible work inside tabs?

Only while the tab is visible: a hidden panel never intersects the viewport. For that case use client:load on the panel that opens, or hydrate manually on click.