How to Choose Astro Client Directives

This guide is part of Astro & SvelteKit Performance, within Framework Performance. In Astro, a UI framework component rendered without a directive is static HTML: it renders on the server and ships no JavaScript. Adding a client:* directive turns it into an island that loads its code and hydrates in the browser. The directive decides when: immediately on page load, when the browser is idle, when the island becomes visible, when a media query matches, or only on the client without server rendering.

The choice matters because every island hydrated at load competes with the browser's other work during the most sensitive window for TBT and early INP. Most islands do not need to be interactive in the first second. Choosing the latest directive that still meets user expectations keeps the main thread free when it matters most.

When islands hydrate under each directive Bar chart of when island hydration starts after navigation for client load, idle and visible directives on a typical article page. When islands hydrate under each directive client:load 600ms client:idle 1800ms client:visible (on scroll) 4200ms client:visible only hydrates if the user scrolls to the island.

Rapid Diagnosis

  • Search for client:load across components and pages; each is an eager island.
  • Record a load trace with CPU throttling; note hydration tasks after load.
  • List islands above and below the fold on key templates.
  • Check island frameworks: multiple frameworks mean multiple runtimes.

Root Cause Analysis

1. client:load by default. Developers add it because it "just works".

2. Below-the-fold islands hydrating eagerly. Work for content users may never reach.

3. client:only overuse. Empty space until JavaScript runs.

4. Heavy islands. Large dependencies hydrate regardless of directive.

Step-by-Step Resolution

1. Default to client:visible below the fold

html
<Testimonials client:visible />
<PricingCalculator client:visible={{ rootMargin: '200px' }} />
<!-- trade-off: a root margin starts hydration slightly before the island is on
     screen, hiding the delay on normal scrolling at the cost of earlier work. -->

2. Use client:idle for soon-needed widgets

Search boxes in the header, cookie preference panels and chat launchers often work well with client:idle, hydrating shortly after load without competing with it.

3. Reserve client:load for immediate interactivity

Above-the-fold controls that users interact with right away (a primary form, a product configurator at the top) justify client:load.

4. Use client:media for responsive-only interactivity

html
<MobileMenu client:media="(max-width: 768px)" />
<!-- Desktop users never download or hydrate the mobile menu. -->

Choosing a client directive Decision sequence for selecting an Astro client directive for an island. Choosing a client directive Does it need to be interactive immediately, above the fold? client:load yes no Is it only interactive at some screen sizes? client:media yes no Is it below the fold? client:visible yes no Is it needed soon but not instantly? client:idle yes no No directive — render as static HTML

Verification

Record a load trace after changes: hydration tasks at load should shrink to only the eager islands. Scroll through the page and confirm islands hydrate and work before users interact. In the field, compare TBT-related metrics and early INP.

Worked Example: A Developer Blog

A developer blog built in Astro used client:load for five islands on every post: a theme toggle, a table of contents with scroll spy, a code copy button, a newsletter form and a comments widget (React with a heavy markdown dependency). Posts shipped 160KB of JavaScript, all hydrating at load; INP p75 was 210ms. The team replaced the theme toggle and copy buttons with small vanilla scripts (no framework), set the table of contents to client:idle, the newsletter form to client:visible, and the comments widget to client:visible with a "load comments" button. JavaScript at load dropped to 9KB, and INP p75 to 70ms.

When Not to Use an Island at All

Many interactive behaviours do not need a framework component. Copy-to-clipboard buttons, theme toggles, disclosure widgets (use details/summary), simple tabs and image lightboxes can be implemented with a few lines of vanilla JavaScript in an Astro <script> tag, which Astro bundles and deduplicates. They are smaller than any framework runtime and run without hydration. Reserve islands for genuinely stateful UI.

html
<button class="copy" data-code="npm install astro">Copy</button>
<script>
  document.querySelectorAll('button.copy').forEach((b) =>
    b.addEventListener('click', () => navigator.clipboard.writeText(b.dataset.code)));
</script>
<!-- trade-off: vanilla scripts lack component state management; fine for simple
     behaviours, but complex UI is clearer as an island. -->

JavaScript at load on blog posts Bar chart of JavaScript loaded at page load for a blog post before and after changing directives and replacing simple islands. JavaScript at load on blog posts 5 islands with client:load 160KB Directives changed 42KB + vanilla scripts for simple UI 9KB

Auditing Islands on an Existing Site

On an existing Astro site, list every island with its directive and framework, page by page. A quick search for client: in the source gives the list; the build output and the Network panel show the chunk each island loads. For each island, ask three questions: does it need to be interactive at all (or could it be static HTML or a small script), when does the user first need it, and how large is its code including dependencies? Changing directives is usually a one-line edit per island, so an audit like this can cut JavaScript at load dramatically in an afternoon. Re-run it periodically, since new islands tend to arrive with client:load copied from examples.

Measuring the Effect of Directive Changes

Compare a throttled lab trace of the page before and after: the hydration work at load should shrink, and later hydration tasks should appear only when islands become visible or the browser is idle. In the field, INP for early interactions and TBT-related signals should improve. Also check that the islands still respond promptly when users reach them; if interactions immediately after scrolling feel delayed, add a root margin to client:visible or switch that island to client:idle.

Common Mistakes

  • client:load on everything. Defeats the islands model.
  • client:only for content. Empty space and slower content; CLS risk.
  • Frameworks for trivial behaviour. Shipping a runtime for a copy button.
  • Ignoring dependency size. A visible-hydrated island with a 200KB dependency still costs 200KB when scrolled to.

Edge Cases

Interactions before hydration. A client:visible island that users interact with very quickly may not be ready; use client:idle or a root margin.

Shared state between islands. Islands are independent; share state with lightweight stores (such as nanostores) rather than a framework context.

View transitions. With Astro's client router, islands can persist across navigations using transition:persist.

Server islands. Astro's server:defer renders slow, personalised server components after the main page, which complements client directives.

FAQ

What is the default if I omit a client directive?

The component renders to static HTML on the server and ships no JavaScript.

When should I use client:only?

Only for components that cannot render on the server, such as those relying on browser APIs during render. Reserve space to avoid layout shifts.

Does client:visible hurt usability?

Rarely, if hydration starts slightly before the island appears. Use a root margin for islands users interact with immediately after scrolling.

Is client:idle reliable on slow devices?

It waits for the browser to be idle, which can take longer on busy pages. Use a timeout or client:visible for important widgets.

Can I pass large data to islands?

Props are serialised into the HTML. Pass small data or IDs, and fetch the rest in the island when it hydrates.

Do islands share a framework runtime?

Islands of the same framework on a page share the runtime chunk. Different frameworks each load their own.

How many islands is too many?

There is no fixed limit; what matters is how much JavaScript hydrates at load. Many small, lazily hydrated islands are fine.

What are server islands?

Components rendered on the server after the main page using server:defer, letting cached static pages include slower, personalised parts.

Can I change a directive conditionally?

Directives are static in the template, but you can render different components or pass a prop to choose behaviour; for responsive cases, client:media covers most needs.

Do directives affect server rendering?

No, except client:only. All other directives server-render the island's HTML; they only control when JavaScript loads.