Skip to content
Astro as islands 3 min read

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 timeout for 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.

DirectiveHydrates whenBest for
client:loadImmediately on page loadCritical, above-the-fold interactivity
client:idleBrowser is idle (or after timeout)Low-priority widgets, near the viewport
client:visibleComponent scrolls into viewAnything well below the fold
client:mediaA media query matchesMobile-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:idle is rarely wrong for secondary, in-viewport widgets.
  • Add a timeout for any idle island a user might interact with quickly, so hydration cannot be starved indefinitely.
  • Prefer client:visible over client:idle for 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:idle for content that must be interactive on first paint (e.g. a primary search or login form); use client:load instead.
  • Profile real pages with throttled CPU to confirm idle hydration is not silently delayed on low-end devices.
Last updated June 14, 2026
Was this helpful?