To lazy load an interactive component in Astro, use the client:visible directive: <Carousel client:visible />. Astro renders the component’s HTML on the server as usual, but only downloads and runs its JavaScript when the component enters the viewport. Add rootMargin to start loading a little earlier, so the island is ready by the time the reader reaches it.
Key Takeaways
client:visibledefers hydration until the component is on screen.client:visible={{ rootMargin: '200px' }}starts loading 200px before it appears.client:idledefers until the browser is idle; good for above-the-fold widgets that aren’t urgent.- Keep
client:loadfor controls people use immediately.
The difference it makes
---
import Testimonials from '../components/Testimonials.tsx';
import Map from '../components/Map.tsx';
---
<!-- Before: both download and hydrate on page load -->
<Testimonials client:load />
<Map client:load />
<!-- After: each loads only when scrolled to -->
<Testimonials client:visible />
<Map client:visible />
With client:load, the framework runtime and both components’ code download, parse and run before the page settles, competing with the content the reader came for. With client:visible, the initial page loads only what’s above the fold. If the reader never scrolls to the map, its JavaScript never loads.
The HTML is identical either way. Users see the server-rendered testimonials immediately; they just aren’t interactive until hydrated.
Load slightly early with rootMargin
Hydration takes a moment. To avoid a reader reaching a carousel that doesn’t respond yet, start early:
<Testimonials client:visible={{ rootMargin: '200px' }} />
rootMargin uses the same syntax as IntersectionObserver. 200px means “load when the component is within 200px of the viewport”. For heavy components (maps, charts), 400px or more is reasonable.
Choosing a directive
| Component | Directive |
|---|---|
| Header search, menu button | client:load or client:idle |
| Newsletter form mid-article | client:visible |
| Comments, embeds, maps at page bottom | client:visible with rootMargin |
| Mobile-only nav drawer | client:media="(max-width: 768px)" |
| Chat widget | client:idle, or load on click |
| Purely visual component | No directive at all |
client:idle waits for requestIdleCallback, with an optional timeout:
<SearchBox client:idle={{ timeout: 500 }} />
That’s a good middle ground for things near the top that don’t need to respond in the first half-second.
Lazy loading something heavier: on click
For very heavy widgets, such as video players or chat, wait for intent. Render a lightweight placeholder and load the real thing when clicked:
<button class="load-chat" type="button">Chat with us</button>
<script>
document.querySelector('.load-chat')?.addEventListener('click', async (e) => {
const btn = e.currentTarget as HTMLButtonElement;
btn.disabled = true;
const { mountChat } = await import('../lib/chat');
mountChat(btn.parentElement!);
btn.remove();
}, { once: true });
</script>
The dynamic import() creates a separate chunk that Astro only serves when the button is pressed.
When not to defer
- Above-the-fold controls. A deferred hero form that ignores the first tap is worse than a slightly larger bundle.
- Components that affect layout once hydrated. If hydration changes height, reserve the space with CSS (
min-height) or the page will jump when it scrolls in, hurting CLS. - Anything with no interactivity. If a component doesn’t need state or events, drop the directive. It then ships zero JavaScript.
Check the result
Build and open DevTools > Network, filtered to JS. Reload: only above-the-fold islands should load. Scroll down and watch the deferred chunks arrive. For a visual breakdown of which components cost the most, see finding what’s bloating your Astro bundle.
Directives and islands are covered in full in the Astro components guide, and the wider picture in the Astro performance guide. The directives reference lists every option.
FAQ
What is the difference between client:visible and client:idle?
client:visible hydrates when the component scrolls into view. client:idle hydrates once the browser is idle after page load, regardless of position.
Does client:visible hurt SEO?
No. The component’s HTML is rendered on the server and is in the page for crawlers. Only the JavaScript is deferred.