Skip to content
Astro best practices 4 min read

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

DirectiveHydrates whenBest forCost
client:loadImmediately on page loadAbove-the-fold, instantly-needed UIHighest—blocks nothing but ships JS eagerly
client:idleBrowser fires requestIdleCallbackLow-priority widgets that can waitDeferred, but still shipped
client:visibleComponent scrolls into viewport (IntersectionObserver)Below-the-fold islandsPay only if seen
client:media={query}A CSS media query matchesResponsive-only UI (mobile menu)Pay only when query matches
client:only={framework}Client-side only; skips SSRComponents that can’t render on the serverNo SSR HTML—watch for layout shift

Tip: client:visible accepts a rootMargin option, 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:visible and only promote to client:load when an island is genuinely needed before scroll.
  • Reserve client:load for one or two truly critical islands per page; every one inflates your critical JS budget.
  • Use client:media to keep desktop-only and mobile-only interactivity off the devices that can’t see it.
  • Prefer SSR plus a directive over client:only; reach for client:only only when server rendering genuinely fails.
  • Always supply a slot="fallback" for client:only islands to prevent cumulative layout shift.
  • Re-run astro build after 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.
Last updated June 14, 2026
Was this helpful?