Documentation
When islands hydrate
client:visible, client:idle and client:media — and what a page stops downloading.
By default an island hydrates as soon as its chunk loads. A client:* directive defers that. It goes on the usage site rather than inside the island, so the same island can be eager on one page and lazy on another without being written twice.
<Chart /> {/* default: on load */}
<Chart client:visible /> {/* scrolled into view */}
<Chart client:idle /> {/* browser goes idle */}
<Chart client:media="(min-width: 60rem)" /> {/* query matches */}What the page stops downloading
A deferred island emits no script tag at all. Its chunk URL travels inside the JSON payload instead, and the page loads a small scheduler that fetches the chunk when the trigger fires.
on load on scroll
───────────────────────── ─────────────────────────
scheduler ~1 KB runtime ~3.4 KB
Chart chunk ~1 KB
───────────────────────── ─────────────────────────
~1 KB gzip fetched only if reachedThe scheduler is deliberately kept clear of the DOM builder and signals. Importing either would drag the whole runtime in with it and there would be no saving left — so that boundary is enforced by a size budget in the test suite rather than by good intentions.
The chunk arrives through a same-origin dynamic import(), which script-src 'self' permits. No inline script, no nonce, and no relaxation of the default policy — the same policy this site runs under.
How each trigger behaves
- client:visible starts hydrating 200px before the element reaches the viewport, so it is usually ready by the time it is on screen.
- client:idle waits for requestIdleCallback, with a 2s cap so a busy page still hydrates.
- client:media hydrates when the query matches — immediately if it already does, otherwise on the next change.
Every trigger degrades to hydrating immediately when the API behind it is missing. A browser without IntersectionObserver gets a working page slightly sooner than intended, never a dead button.
Things that are errors
- Two directives on one usage. There is no sensible answer to client:idle client:visible, and inventing a precedence rule to memorize would be worse than saying so.
- A directive on a plain element. Only islands hydrate, and rendering it as a stray attribute would look correct while never working.
- client:media without a query, or an unknown directive. TypeScript catches both first; the runtime check covers JavaScript and spread props.
The directive is stripped before the island runs, so an island never sees client:visible among its props and needs no awareness that any of this exists.
A page with no deferred island is byte-for-byte what it was before the feature existed: no scheduler is loaded, and the payload carries no strategy field. Eager stays the default because it is the right one for a button above the fold.
Something wrong in the framework itself rather than the page? Open an issue on GitHub.