Overview
Astro’s islands architecture ships zero JavaScript by default and lets you opt into client-side hydration per component. Each interactive component is an island: isolated, independently hydrated, surrounded by static HTML. This model keeps page weight small on content-heavy sites and gives you precise control over when the browser pays the JS parse and execute cost.
Understand what an island actually is
An island is a framework component (React, Vue, Svelte, Solid, or Preact) that Astro server-renders to HTML and then selectively rehydrates on the client. Outside that island, the page is inert HTML. The browser downloads the island’s framework runtime and component bundle only when the directive fires, not on page load.
Without a client:* directive, the component renders to HTML once at build time (or request time for SSR routes) and ships no JS. That is the correct default for decorative or display-only components.
Use client:load for above-the-fold interactivity
client:load hydrates the component immediately when the page loads. Use it for interactive elements the user will touch within the first second: a theme toggle, a sticky search bar, an auth button. Avoid it for anything below the fold; every client:load island adds to the blocking parse cost.
<ThemeToggle client:load />Use client:idle for non-critical widgets
client:idle waits for requestIdleCallback before hydrating. The browser hydrates these islands after layout-critical work is done. Use it for elements the user may interact with but does not need instantly: a newsletter form in a sidebar, a “share” button, a non-critical tooltip component.
<NewsletterForm client:idle />Use client:visible as the default for below-the-fold islands
client:visible fires an IntersectionObserver and hydrates when the component scrolls into the viewport. This is the right default for most islands below the fold: comment sections, embedded charts, media players, interactive demos. The user gets the island hydrated just before they need it.
<CommentsSection client:visible />Use client:media for responsive interactive regions
client:media accepts a CSS media query and hydrates only when that query matches. Use it for components that only make sense at certain viewport sizes: a mobile drawer nav that does not exist on desktop, a swipe carousel for narrow screens.
<MobileMenu client:media="(max-width: 768px)" />Use client:only when server rendering would break the component
client:only skips server rendering entirely and runs the component only on the client. Use it for components that depend on browser APIs: a chart library that reads window.innerWidth on mount, a canvas drag interface, a WebGL renderer. Pass the framework string explicitly.
<DragCanvas client:only="react" />Do not use client:only as a workaround for hydration errors in components that could server-render correctly. Fix the component; client:only costs a full layout flash.
Keep a per-page JS budget
Track how much JS each island adds. Run astro build and inspect the generated dist/ bundles. Each framework runtime ships once per framework per page: React adds roughly 45 KB (gzipped) the first time a React island appears. A second React island on the same page reuses that runtime; it is not doubled.
Rules for the budget:
- One framework per page when possible. Mixing React and Svelte islands on the same page ships two runtimes.
- Prefer
client:visibleandclient:idleoverclient:loadto delay runtime parse time. - Shared state between islands belongs in a store like Nano Stores, not in React context. Islands are isolated; React context does not cross island boundaries.
- Audit with astro-performance rules before shipping a page past 50 KB JS.