client:idle
The client:idle directive tells Astro to hydrate an interactive island only once the browser has finished its critical startup work and entered an idle period. Because Astro ships zero JavaScript by default, every island you opt in is a deliberate cost — client:idle lets you keep that cost off the critical path. It is the ideal choice for components that are interactive but not essential to the first impression, such as newsletter sign-ups, “back to top” buttons, or secondary widgets below the fold.
How client:idle works
When you attach client:idle to a framework component, Astro renders its HTML on the server (so the content is visible immediately) and emits a tiny inline script that schedules hydration via the browser’s requestIdleCallback API. The component’s JavaScript and framework runtime are only fetched and executed after the main thread is free, so they never compete with rendering, fonts, or higher-priority islands.
In browsers that do not support requestIdleCallback (notably older Safari versions), Astro falls back to a setTimeout, so hydration still happens reliably — just without the precise idle scheduling.
---
// src/pages/index.astro
import NewsletterSignup from "../components/NewsletterSignup.jsx";
import Counter from "../components/Counter.jsx";
---
<article>
<h1>Welcome to DevCraftly</h1>
<p>Static, zero-JS content renders and paints instantly.</p>
<!-- Important UI: hydrate immediately -->
<Counter client:load />
<!-- Low priority: hydrate when the browser is idle -->
<NewsletterSignup client:idle />
</article>
The NewsletterSignup island is fully rendered in the initial HTML, so users can read it the moment the page paints. Its interactivity simply “arrives” a few hundred milliseconds later once the browser is no longer busy.
Controlling the timeout
By default requestIdleCallback may wait indefinitely if the main thread stays busy. To guarantee an upper bound, pass a timeout option (in milliseconds). Astro forwards it to the underlying requestIdleCallback call.
---
import SearchBox from "../components/SearchBox.svelte";
---
<!-- Hydrate when idle, but no later than 2 seconds -->
<SearchBox client:idle={{ timeout: 2000 }} />
Tip: Use
timeoutfor components that users might reach for early. Without it, a long-running script could starve hydration on slow devices and leave the island unresponsive.
When to reach for client:idle
client:idle sits between eager and on-demand strategies. Use the table below to pick the right directive.
| Directive | Hydrates when | Best for |
|---|---|---|
client:load | Immediately on page load | Critical, above-the-fold interactivity |
client:idle | Browser is idle (or after timeout) | Low-priority widgets, near the viewport |
client:visible | Component scrolls into view | Anything well below the fold |
client:media | A media query matches | Mobile-only / desktop-only UI |
Reach for client:idle when the island is visible on first paint (so client:visible would over-defer) but is not urgent enough to block the main thread during load.
Verifying the deferred hydration
You can confirm the behavior in the browser’s Performance or Network panel: the framework chunk for an idle island loads after the document’s initial work settles. A quick way to observe it in code is to log inside the component’s mount lifecycle.
// src/components/NewsletterSignup.jsx (React)
import { useEffect, useState } from "react";
export default function NewsletterSignup() {
const [email, setEmail] = useState("");
useEffect(() => {
console.log("NewsletterSignup hydrated at", Math.round(performance.now()), "ms");
}, []);
return (
<form onSubmit={(e) => e.preventDefault()}>
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="[email protected]" />
<button type="submit">Subscribe</button>
</form>
);
}
Output:
NewsletterSignup hydrated at 412 ms
The timestamp lands well after first paint, confirming the island waited for an idle window instead of hydrating eagerly.
Best Practices
- Default to the cheapest directive that meets the UX requirement —
client:idleis rarely wrong for secondary, in-viewport widgets. - Add a
timeoutfor any idle island a user might interact with quickly, so hydration cannot be starved indefinitely. - Prefer
client:visibleoverclient:idlefor components below the fold; there is no reason to hydrate something the user may never scroll to. - Keep idle islands small — deferring a heavy component still pays the full bundle cost, just later. Split or lazy-load where you can.
- Never use
client:idlefor content that must be interactive on first paint (e.g. a primary search or login form); useclient:loadinstead. - Profile real pages with throttled CPU to confirm idle hydration is not silently delayed on low-end devices.