Choosing the Right Hydration
Astro ships zero JavaScript by default. Every framework component you author renders to static HTML during the build unless you explicitly opt it into hydration with a client:* directive. That single decision—made per island—is the biggest lever you have over your page’s runtime cost. Choosing the wrong directive ships interactivity nobody needs; choosing none ships a dead button. This page helps you match each island to exactly the directive it deserves.
The mental model: islands, not pages
In the islands architecture, an interactive component is a small, isolated region of the page that hydrates independently. The static shell around it stays as plain HTML. Your job is to decide, for each island, when its JavaScript should download and execute. A directive answers that question—it never makes a component “more” or “less” interactive, only earlier or later.
---
// src/pages/index.astro
import Newsletter from "../components/Newsletter.jsx";
import SearchBox from "../components/SearchBox.jsx";
import StockTicker from "../components/StockTicker.vue";
---
<SearchBox client:load />
<Newsletter client:visible />
<StockTicker client:idle />
Each component here is hydrated on a different schedule. The search box is needed immediately, the newsletter only when scrolled into view, and the ticker once the browser is idle.
The directives at a glance
| Directive | Hydrates when | Best for | Cost |
|---|---|---|---|
client:load | Immediately on page load | Above-the-fold, instantly-needed UI | Highest—blocks nothing but ships JS eagerly |
client:idle | Browser fires requestIdleCallback | Low-priority widgets that can wait | Deferred, but still shipped |
client:visible | Component scrolls into viewport (IntersectionObserver) | Below-the-fold islands | Pay only if seen |
client:media={query} | A CSS media query matches | Responsive-only UI (mobile menu) | Pay only when query matches |
client:only={framework} | Client-side only; skips SSR | Components that can’t render on the server | No SSR HTML—watch for layout shift |
Tip:
client:visibleaccepts arootMarginoption, e.g.client:visible={{ rootMargin: "200px" }}, so hydration starts just before the island enters the viewport. This hides the tiny hydration delay from users.
When to reach for each directive
client:load — needed right now
Use it sparingly for components users interact with before they scroll: a primary search field, a sticky cart button, an authentication widget. Anything client:load adds to your critical JavaScript budget, so treat each one as a deliberate cost.
client:idle — important but not urgent
client:idle waits for the main thread to settle before hydrating, keeping the page responsive during initial paint. It’s ideal for things like analytics-driven banners or a “back to top” control that the user won’t touch in the first second.
client:visible — the default for most islands
This is the workhorse. Because hydration is gated by an IntersectionObserver, JavaScript for a footer accordion or a comments widget never downloads if the visitor leaves before reaching it. For long content pages this is often the single biggest win.
client:media — interactivity that depends on screen size
A hamburger menu only matters on small screens; a desktop navigation drawer only on large ones. Hydrating behind a media query means mobile visitors never pay for desktop-only JS, and vice versa.
---
import MobileMenu from "../components/MobileMenu.svelte";
---
<MobileMenu client:media="(max-width: 768px)" />
client:only — when the server can’t render it
Some components depend on window, localStorage, or a charting library that throws during SSR. client:only skips server rendering entirely. You must name the framework so Astro knows which renderer to load.
---
import Chart from "../components/Chart.jsx";
---
<Chart client:only="react" />
Because there’s no server-rendered markup, provide a fallback to avoid layout shift:
<Chart client:only="react">
<div slot="fallback" class="chart-skeleton">Loading chart…</div>
</Chart>
A decision checklist
Walk this list top to bottom and stop at the first match:
Does it render on the server cleanly? no -> client:only
Is it interacted with before any scroll? yes -> client:load
Is it only relevant at a screen size? yes -> client:media
Is it below the fold? yes -> client:visible
Otherwise (low priority, on screen) -> client:idle
Verifying your choices
After wiring up directives, audit the JavaScript actually shipped. Build the site and inspect the per-route bundle report.
astro build
Output:
17:42:10 [build] 12 page(s) built in 1.84s
17:42:10 ▶ /index
└─ /_astro/SearchBox.abc123.js (4.2 kB) [client:load]
17:42:10 ▶ /blog/[slug]
└─ /_astro/Comments.def456.js (6.1 kB) [client:visible]
If a route lists a script you didn’t expect, an island is over-hydrated. Demote it to a lazier directive or remove the directive entirely.
Warning: A component with no
client:*directive ships zero JavaScript—its event handlers will never run. If a button “does nothing,” the missing directive is almost always why.
Best Practices
- Default to
client:visibleand only promote toclient:loadwhen an island is genuinely needed before scroll. - Reserve
client:loadfor one or two truly critical islands per page; every one inflates your critical JS budget. - Use
client:mediato keep desktop-only and mobile-only interactivity off the devices that can’t see it. - Prefer SSR plus a directive over
client:only; reach forclient:onlyonly when server rendering genuinely fails. - Always supply a
slot="fallback"forclient:onlyislands to prevent cumulative layout shift. - Re-run
astro buildafter directive changes and read the bundle report to confirm no island over-hydrates. - Keep islands small—hydrating a focused component costs far less than hydrating a sprawling one.