komal.nakrani
Browse Themes
ThemesCategoriesDocsBlogTips & Tricks
Performance••By Komal Nakrani

Lazy-Load Astro Islands with client:visible

Lazy load Astro islands with client:visible so a component's JavaScript downloads only when it scrolls into view. rootMargin, client:idle and when not to defer.

#Astro#Islands#Performance#Client Directives

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:visible defers hydration until the component is on screen.
  • client:visible={{ rootMargin: '200px' }} starts loading 200px before it appears.
  • client:idle defers until the browser is idle; good for above-the-fold widgets that aren’t urgent.
  • Keep client:load for 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.