Load less JavaScript in Astro with client directives
Use Astro client directives to hydrate interactive islands only when users need them, keeping the rest of the page static.
23 Feb 2026, 13:57 UTC

An interactive button that loads too early
You add a React dropdown or notification bell to an Astro page. By default, Astro renders components to static HTML and ships no client-side JavaScript. If you forget a client directive, the button looks interactive but does nothing. If you add client:load to every component, the browser downloads and executes JavaScript for everything on first load, erasing Astro's performance advantage.
These directives are available in current stable releases of Astro. The useful takeaway is to match each interactive island to the moment it should hydrate—immediately, on idle, on scroll into view, or never as an Astro wrapper—so the page stays static until the user actually needs interactivity.
How Astro treats components by default
Astro components and framework components from React, Vue, Svelte, or Solid are rendered to HTML at build time. The result is static HTML with no script tags for those components unless you add a client directive. Astro calls each hydrated client component an island because it loads and initializes independently.
Choose a directive based on when interaction is needed
| Directive | When JavaScript runs | Best for |
|---|---|---|
client:load | Immediately on page load | Above-the-fold interactivity that cannot wait |
client:idle | When the browser's main thread is idle | Non-critical widgets below the fold |
client:visible | When the component enters the viewport, using IntersectionObserver | Contact forms, chat buttons, or carousels further down the page |
client:only | Immediately, without the Astro HTML wrapper | Embeddable widgets where Astro static HTML is unnecessary |
Worked example: defer a contact form until it is visible
Suppose you have a React contact form at the bottom of a landing page. You do not need its JavaScript until the user scrolls near it.
1. Add the component
---
// src/components/ContactForm.tsx
import { useState } from 'react';
export default function ContactForm() {
const [message, setMessage] = useState('');
return (
<form>
<label>Message</label>
<textarea value={message} onChange={e => setMessage(e.target.value)} />
<button type='submit'>Send</button>
</form>
);
}
2. Import it in an Astro page with client:visible
---
// src/pages/index.astro
import ContactForm from '../components/ContactForm.tsx';
---
<main>
<h1>Landing page</h1>
<p>Most of this page is static HTML.</p>
</main>
<!-- The form stays static HTML until it enters the viewport -->
<ContactForm client:visible />
Where to run: Create the files in an Astro project directory, then run npm install and npm run dev from your terminal as a normal user. No elevated permissions are required.
Expected check: Open the page in a browser, open DevTools to the Network panel, reload, and confirm no JavaScript chunk for the contact form loads initially. Scroll the form into view; the chunk should appear only then.
Risk: If the form must work without JavaScript for users who never scroll, provide a static fallback or progressive enhancement, because client:visible defers all client behavior.
3. Verify the build output
Run npm run build or astro build from the project directory. Inspect the build output for client entrypoints. Each island with a directive should generate a separate client entrypoint rather than bundling everything into the page's main JavaScript.
Limitations and what can go wrong
- Hydration mismatches: If the server-rendered HTML differs from the client's first render, the framework may throw a mismatch error and re-render the subtree. Keep output deterministic and avoid browser-only APIs like window during the first render.
- IntersectionObserver support: client:visible relies on IntersectionObserver. Older browsers may need a polyfill.
- Island isolation: Clicking a button in one island does not automatically update another. Share state through props passed from the Astro page, or use an external store.
- Too much JavaScript: Using client:load on many components sends more JavaScript to the browser and can erase Astro's performance advantage.
Check the result before you ship
- Open the page in Chrome or Firefox DevTools.
- Record the Network panel while reloading the page.
- Confirm that components with client:visible or client:idle do not request their JavaScript chunks until the expected trigger.
- Run Lighthouse and compare Total Blocking Time against a version that uses client:load everywhere.
If a component never becomes interactive after scrolling, check the browser console for hydration errors and confirm the directive is spelled correctly.
Rollback
This change only edits component markup and configuration. To revert, remove the client directive or replace it with client:load, then rebuild. No database or server state is modified.
Decide per component, not per site
Astro's default of zero client-side JavaScript is the performance baseline. Use client directives to opt individual islands into hydration only when the interaction matters. Start with client:visible for below-the-fold widgets, measure with DevTools and Lighthouse, and reserve client:load for components that must work the instant the page appears.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.