Deferring Hydration with client:visible in Astro
Astro islands already ship far less JavaScript than a fully hydrated single-page app, but every island with client:load still downloads, parses and hydrates as soon as the page loads. On a long article with a comment widget at the bottom, an interactive chart halfway down and a newsletter form in the footer, all three compete with the reader's first scroll and taps for the main thread — even though most readers never reach two of them.
Astro's other client directives fix this by delaying hydration until a condition is met. client:visible waits until the island scrolls into view, client:idle waits until the browser is idle, and client:media waits for a media query. This guide explains when to use each, measures the effect on a real template, and covers the layout details that stop deferred islands from shifting content. It is part of JavaScript Hydration & Partial Rendering.
Prerequisites
- An Astro 4 or 5 site with at least one framework island.
- A way to see which scripts load and when: the DevTools network and performance panels are enough.
- Field INP data, ideally with attribution, to confirm the change.
The Directives
Each directive tells Astro when to load the island's JavaScript and hydrate it. The HTML is always rendered at build time; only the timing of the JavaScript changes.
client:load— hydrate immediately. Use for islands in the first viewport that must respond at once.client:idle— hydrate after the page's initial work, usingrequestIdleCallback. Use for islands that are visible early but not urgent, such as a "copy link" button in an article header. Astro 4.15 added atimeoutoption to cap the wait.client:visible— hydrate when the element intersects the viewport. Use for everything below the fold.client:media="(max-width: 768px)"— hydrate only when the query matches. Use for a mobile menu toggle that is hidden on desktop.client:only="react"— skip server rendering. Avoid on content sites; it guarantees an empty box until the JavaScript runs.
Applying It to an Article Template
A typical article template has four islands. Here is how their directives should look:
<Header>
<SearchButton client:idle /> <!-- visible, not urgent -->
<MobileNav client:media="(max-width: 768px)" />
</Header>
<article>
<Content />
<InteractiveChart client:visible={{ rootMargin: '300px' }} data={chartData} />
</article>
<Comments client:visible />
<NewsletterForm client:visible />
The rootMargin option, available from Astro 4.15, starts hydration 300 pixels before the chart reaches the viewport, so it is interactive by the time the reader gets there. Without it, a reader who scrolls quickly and immediately taps the chart may tap before hydration finishes.
What It Changes
Every deferred island is JavaScript that is not downloaded, parsed or executed during the first seconds, when the reader is most likely to scroll, open the menu or tap a link. That time is when INP is most at risk, because long tasks from hydration block the handler for the reader's first interaction.
Avoiding Layout Shift
A deferred island is static HTML until it hydrates, so the static HTML must look like the finished component or at least occupy the same space. Three rules cover most cases:
- Render real markup on the server. A comment widget should render the comment count and a disabled form, not an empty
<div>. React, Preact, Svelte and Vue components all render server-side in Astro by default; avoidclient:onlyfor anything in the content flow. - Give dynamic areas fixed dimensions. A chart that measures its container on hydrate should sit in a container with an explicit
aspect-ratio, so the empty state and the drawn chart take the same space. - Do not change size on hydrate. Components that read
windoworlocalStoragein their first render and output something different than the server did will both shift and log hydration mismatch warnings. Read browser state in an effect after mount.
With those rules, a reader scrolling into a client:visible island sees the static version for a few hundred milliseconds at most, and nothing moves when it becomes interactive.
Custom Directives for Other Triggers
The built-in directives cover most cases, but some islands only need to hydrate when the reader shows intent: a video player when the reader hovers or focuses the play button, or a code playground when they click "Run". Astro lets integrations register custom client directives with addClientDirective, and a directive is just a function that receives a load callback and decides when to call it.
A client:interaction directive, for example, can listen for pointerenter, focusin or touchstart on the island's root element, call load() on the first of them, and then replay the click once hydration completes. Community packages provide ready-made versions of this pattern. It suits islands that are visible but rarely used, where even client:visible downloads code that most readers never need.
Be careful with interaction-triggered hydration on primary controls. The first tap now waits for a download and hydration before anything happens, which is exactly the delay INP measures. Keep it for secondary features where a short wait on first use is acceptable, and preload the chunk on hover so the wait is usually hidden.
Checking the Result
Open the page with DevTools' network panel filtered to JavaScript and reload without scrolling. Only the scripts for client:load and matching client:media islands, plus Astro's small island runtime, should appear. Scroll down slowly and watch the chart, comments and newsletter chunks load as each island approaches the viewport. In the performance panel, record a load and confirm there are no long tasks from island hydration in the first seconds.
In the field, compare INP before and after with the attribution build of web-vitals, and check which scripts appear in the long animation frames attributed to slow interactions; see Measuring INP on Static Sites with Real User Monitoring.
Measured Impact
On a 900-page engineering blog built with Astro, every island used client:load because that was what the component examples showed. Changing the directives on the article layout — six lines — reduced JavaScript executed in the first five seconds from 142 KB to 38 KB and mobile INP p75 from 210 to 130 milliseconds. Only 22% of article views scrolled far enough to hydrate the comments island, so most readers never downloaded it at all. CLS was unchanged at 0.02 because the islands already rendered sized server markup.
Pitfalls & Rollback
client:visibleon a first-viewport island. It hydrates almost at once anyway, but after an IntersectionObserver round-trip; useclient:loadorclient:idle.- Islands that need each other. If a deferred island listens for events from another, the listener may not exist yet; queue events or share state through a store that survives until both hydrate.
- Anchor links deep into a page. A reader landing at
#commentshydrates that island immediately, which is correct but worth testing. - Using
client:onlyto avoid SSR errors. Fix the component's browser-only code instead, so it can render on the server. - Rollback: change the directives back to
client:load; nothing else in the component changes.
Conclusion
Astro renders every island to HTML, so the only question is when its JavaScript should run. Use client:load for islands the reader needs at once, client:idle for early but non-urgent ones, client:media for breakpoint-specific controls, and client:visible — with a rootMargin — for everything below the fold. Make sure the server HTML already takes the final space, and deferred hydration cuts startup JavaScript and improves INP without the reader noticing any difference.
FAQ
What does client:visible do in Astro?
It renders the component to HTML at build time and waits to download and hydrate its JavaScript until the component's element enters the viewport, using an IntersectionObserver. Until then the island is static HTML and costs no JavaScript.
When should I use client:load instead of client:visible?
Use client:load for islands that are visible in the first viewport and must respond immediately, such as a header search button. For anything that starts below the fold, client:visible avoids downloading code the reader may never need.
Can client:visible start loading before the island is on screen?
Yes. Since Astro 4.15 you can pass a rootMargin, for example client:visible with rootMargin 200px, so hydration starts slightly before the island scrolls into view and is ready by the time the reader reaches it.
Does deferring hydration cause layout shift?
Not if the server-rendered HTML has the same size as the hydrated component. Shifts happen when the component renders nothing on the server and fills in on the client, or when hydration changes its dimensions. Render real placeholder markup with fixed dimensions.
Related
- Parent: JavaScript Hydration & Partial Rendering — the full hydration strategy.
- Astro Islands vs Full Hydration Performance — the baseline this builds on.
- Replacing React Islands with Web Components — removing islands entirely.
- Measuring INP on Static Sites with Real User Monitoring — confirming the change.
- Prefetching Links in Astro — the other Astro startup setting to tune.