Astro Islands are the right architectural choice for almost every marketing site. But 90% of teams use `client:load` when they should use `client:visible`. This post explains the four directives, shows you the decision tree, and walks you through three real Velocity X examples.
Astro's philosophy is simple: send zero JavaScript by default. Every page is static HTML. The browser renders it instantly. No hydration overhead. No bundle bloat. But sometimes you need interactivity. A dropdown menu. A form. A slider. An animated background shader. That's where islands come in. You carve out a React (or Vue, or Svelte) component, mark it as interactive, and Astro ships just that component as JavaScript. The rest of the page stays static HTML. You get 99% static site performance with 1% app interactivity. It's elegant. It's performant. And almost everyone implements it wrong.
The problem: five different ways to hydrate an island (`client:load`, `client:idle`, `client:visible`, `client:only`, and `server` — technically six). Most teams reach for `client:load` out of habit. The page blocks on JavaScript. Everything waits. Performance tanks. The real move is to pick the directive that matches the island's purpose. A header nav? `client:load` — it's above the fold and users need it immediately. A gallery below the fold? `client:visible` — it hydrates when it scrolls into view. A non-critical post-load script? `client:idle`. An interactive 3D shader that can't SSR? `client:only`. Pick right and you ship fast. Pick wrong and you're one of the 90%.
The Four Directives Explained
client:load — Immediate hydration
Hydrate the component as soon as the page loads. No waiting. The browser prioritizes this island's JavaScript in the initial bundle. By the time the DOM is interactive, this component is hydrated and ready. Use `client:load` for components that are above the fold, critical to UX, and needed before the user can interact with anything else.
Performance impact: adds to your critical bundle. TTI (Time to Interactive) increases slightly because the browser must download and execute this JavaScript before the page feels "done."
client:idle — Hydrate after page load
Hydrate the component when the browser's main thread is idle. Not immediately, not on scroll — just whenever the browser finishes other work. Usually hits within 2–8 seconds on a fast connection, longer on 4G. Use `client:idle` for components that are important but not critical to initial interactivity. Example: a contact form below the fold that users will interact with after reading.
Performance impact: minimal. The JavaScript loads in the background. The page becomes interactive faster because you're not blocking on this component's bundle.
client:visible — Hydrate on scroll into view
Hydrate the component only when it enters the viewport. Uses Intersection Observer under the hood. The component is inert HTML until the user scrolls to it. Perfect for carousels, galleries, or interactive visualizations that live below the fold. Most sites hydrate these with `client:load` (mistake) — they should use `client:visible`.
Performance impact: huge win. You never ship the JavaScript for islands the user doesn't scroll to. On a 3000-word landing page with three interactive sections? You skip two of them if the user bounces early. TTI improves because you're not shipping unnecessary bundles.
client:only — No SSR, shipped as-is
Don't attempt to server-render this component. Ship it directly as JavaScript and hydrate immediately on the client. Use `client:only` for components that rely on browser APIs (canvas, WebGL, Web Audio, Intersection Observer at the component level) that don't make sense to SSR. Example: a 3D shader, a real-time audio visualizer, anything that needs `window` or `document` to function.
Performance impact: moderate. You're not getting the SSR performance win of static HTML. The component renders entirely on the client. But for things that can't be statically rendered (like interactive shaders), this is the right call. Total bundlesize is usually smaller because the component doesn't have dual code paths (server + client).
The Decision Tree
Pick a directive in 30 seconds using this mental model:
- Is the component above the fold AND critical to UX? → `client:load`
- Does the component require browser APIs that can't be SSR'd? → `client:only`
- Is the component below the fold AND only visible on scroll? → `client:visible`
- Is the component important but not critical, and you want to load it in the background? → `client:idle`
- Is the component pure Astro (no interactivity)? → `server` (don't use a directive at all)
That's it. Follow this tree and you'll get 95% right.
Three Real Velocity X Examples
Example 1: HeaderApp = client:load
The site header is interactive. Users click dropdowns, toggle dark mode, navigate menus. The header is above the fold on every page. Users interact with it immediately. No question: `client:load`.
<!-- src/layouts/Layout.astro -->
<HeaderApp client:load />HeaderApp hydrates on page load. By the time the page is interactive, the nav works, dropdowns open, theme toggle works. The performance cost is baked into your critical bundle, but it's necessary — the user needs the header to function.
Example 2: RadialGallery = client:visible
Velocity X features an interactive radial gallery showcasing design work. It's a React component with smooth animations and click handlers. But it's 2000px down the page. Most users see it, some don't. If a user bounces before scrolling to the gallery, you shipped JavaScript for nothing.
<!-- src/pages/index.astro -->
<RadialGallery client:visible />RadialGallery hydrates only when it scrolls into view. Initial page load is faster. TTI is faster. The gallery loads silently in the background once visible. By the time the user can interact with it, it's ready. Performance win across the board.
Example 3: BrandShader = client:only
The brand strategy page features an animated 3D gradient shader rendered with Three.js (or raw WebGL). It's a visual hero element that needs canvas rendering, real-time animation, and browser-only APIs. No SSR equivalent exists — you can't pre-render a 3D shader as static HTML.
<!-- src/pages/brand-strategy.astro -->
<BrandShader client:only />BrandShader is `client:only`. Astro skips SSR (which would fail anyway) and ships the component as pure client-side JavaScript. It renders immediately on the client. The user sees the shader animate. No wasted server cycles. No hydration mismatch.
Performance Math: TTI Impact
Assume a landing page with three interactive islands:
- HeaderApp: 15kb gzipped
- RadialGallery: 35kb gzipped
- ContactForm: 8kb gzipped
Scenario A: All three use `client:load`
Total bundle: 58kb. Browser downloads all of it before the page is interactive. On 4G (1 Mbps = ~125 KB/s), that's ~470ms just to download, plus parse/execute time. TTI = ~800ms.
Scenario B: HeaderApp `client:load`, RadialGallery `client:visible`, ContactForm `client:idle`
Critical bundle (HeaderApp): 15kb. Page is interactive in ~200ms. RadialGallery and ContactForm load in the background. User doesn't notice because they're not immediately visible or needed. TTI = ~200ms. The perceived performance improvement is 4× faster.
For 90% of landing pages, Scenario B is the real-world win. You're not shipping 58kb of JavaScript upfront — you're shipping 15kb, and the rest trickles in as needed. The page feels instant.
Six FAQs
Can I change a directive mid-development?
Yes. Change it, save, and the dev server hot-reloads. No build restart needed. Test different directives on a slow 3G throttle to see the performance difference in real time.
What if a `client:visible` component needs to be interactive before it scrolls into view?
That's a sign it should be `client:load` or `client:idle` instead. The directive must match the actual UX. If the component is critical before scroll, don't lazy-load it.
Does `client:only` mean the component won't show on the server?
Correct. `client:only` components don't render to HTML at build time. They render only in the browser after JavaScript loads. For interactive shaders or real-time data, that's fine. For SEO-critical content, use a different directive or SSR the component with Astro itself.
Can I mix directives on the same page?
Yes. That's the whole point. HeaderApp `client:load`, gallery `client:visible`, form `client:idle`, shader `client:only` — all on one page. Astro orchestrates the hydration order. You get the performance win of only shipping critical JavaScript upfront.
What about nested components inside an island?
If HeaderApp is `client:load` and it renders 10 child components, those children are included in HeaderApp's bundle. The directive applies to the root island, and everything inside it hydrates together. Don't nest huge component trees inside a single island if you want granular lazy-loading.
Does `client:idle` have browser support?
Yes. `client:idle` uses `requestIdleCallback` if available, falls back to a timer. Works everywhere. Not a cutting-edge API.
The Bottom Line
Astro Islands are the right move for marketing sites. Static HTML + tiny interactive pieces. But the directives matter. The default instinct to use `client:load` everywhere is wrong. Get deliberate: above the fold and critical? `client:load`. Below the fold? `client:visible`. Non-critical and post-load? `client:idle`. Needs browser APIs? `client:only`. Follow this rule and your TTI improves 2–4×. Your page feels faster. Your Core Web Vitals improve. You win.
This is how Velocity X builds stay under 50kb of critical JavaScript while shipping full interactivity. The same pattern applies to every Astro site. Pick the right directive for each island, and suddenly static sites feel like apps.
Building your next site with Astro Islands? Check out Aidxn Design's web services to discuss performance-first architecture, or read more about how View Transitions amplify the SPA feel on top of MPA performance.