Regions
Client islands
Mark an import with `wake`, and that component ships JS on a schedule. The binding becomes a portable island that you can pass around.
On this page
A client island is a component that becomes interactive. You mark its import with wake. ogygia then ships JS for that component on a schedule. The remainder of the page stays server HTML with zero client JS.
When to use
Use a client island when a part of the page must react to the user: a counter, a menu, a form widget, a chart. If the part never needs JS, do not mark it (it stays HTML), or make it a lake.
<script>
import Counter from './Counter.svelte' with { wake: 'load' };
</script>
<Counter start={3} />The remainder of the page does not change. Only Counter gets JS, and only on the schedule you set.
The schedules
wake takes a timing word. The word says when the JS of the island runs:
| Value | JS runs |
|---|---|
load | immediately |
visible | when scrolled into view |
interaction | on the first pointer, key, or focus inside it. The click that wakes it replays. Typed text stays. |
There are two more values (idle and CSS media queries). There is also margin tuning for visible, and there are rules for nested regions. The full schedule reference is in Timing & nesting.
Each demo below is a real island on this page, with a different schedule. Reload with JS off, and the markup remains. The widgets stay static.
wake: 'load'Live since —
wake: 'idle'Idle after …
--:--:--
wake: 'visible'In view · —
The $effect of each island writes the Live since … / Idle after … / In view … stamps after it wakes. The stamps show that the component hydrated. They are not baked values. The idle and visible islands wake later than load.
The binding is the island
When you mark an import, ogygia rewrites the binding itself into a virtual island wrapper. The template still says <Counter />. But Counter is now a portable component. You can use it in each place a Svelte component goes: assign it, pass it, render it in a list.
<script>
import A from './A.svelte' with { wake: 'load' };
</script>
<A start={3} />
<!-- dynamic -->
{@const Active = A}
<Active start={3} />
<!-- in a list -->
{#each [{ Comp: A, props }] as { Comp, props }}
<Comp {...props} />
{/each}<svelte:component> is not necessary. This is Svelte 5. The marked import is a normal component value. Thus islands combine the same way that Svelte components combine. There is no special island slot or registry.
Dedupe by identity
Wrappers dedupe by component path plus strategy and options. They do not dedupe by tag site or host. One marked import, used one thousand times across a page, makes one wrapper module and one client entry chunk. Each instance shares that entry URL. Each instance still gets its own region and its own props payload at SSR. The signature of a deferred island stays per-instance.
Thus <A /> × 1000 ships one module, not one thousand.
Here is one marked import, rendered three times on this page. The three share one client entry chunk. Each hydrates into its own independent state:
Live since —
Live since —
Live since —
Props, not children
Props are real Svelte props into the wrapper. devalue serializes them for the island payload. Thus they must be serializable: numbers, strings, plain objects, arrays. These cannot cross the boundary:
- Functions (an
onSavecallback): devalue cannot carry a closure. Do the work inside the island, or call a remote function. - Host children and snippets: put the UI of the island, and its lakes, inside the
.sveltefile of the island. The one exception is the reservedogygiaFallbacksnippet on a server island. - Live class instances: unless the class opts in as shared state.
See Constraints for the full list of what crosses the boundary.
A worked example: a cart badge
A header badge that shows a cart. It hydrates on load, so it is live immediately:
<script>
let { count = 0 } = $props();
</script>
<span class="badge">{count}</span><script>
import CartBadge from '$lib/CartBadge.svelte' with { wake: 'load' };
</script>
<CartBadge count={items.length} />A different “Add to cart” island can update this badge. The two share one live object. See Shared state.
The boot island: run code, not UI
On a csr = false page, the script of +page.svelte / +layout.svelte never runs in the browser. Top-level onMount, $effect, listeners, telemetry, and store hydration are all inert. See Constraints. An island is the only place where client code runs. Thus, when you must run code and not render UI, make an island that renders nothing. That is a boot island: a component with no markup, whose only job is its $effect.
<script>
let { dsn } = $props();
$effect(() => {
const t = init_telemetry(dsn);
return () => t.stop(); // teardown runs when the island unmounts
});
</script>
<!-- no markup --><script>
import Telemetry from '$lib/Telemetry.svelte' with { wake: 'load' };
</script>
<Telemetry dsn={data.dsn} />
{@render children()}- Values are props. The page script is inert. Thus, give the island what it needs as serializable props (
dsn={data.dsn}). This is the same boundary rule as for each island. - Lifecycle follows the schedule. A page-level boot island runs its
$effectagain on each navigation, because the page remounts. A layout-level boot island usually must continue across navigation. It must not restart a socket on each route. Addkeep, and its live self moves onto the new page. It does not remount:
import Telemetry from '$lib/Telemetry.svelte' with { wake: 'load', keep: 'telemetry' };A module store often needs no boot at all
Top-level code in a .svelte.ts module runs when the first island imports it. If an island reads your store, the store initializes itself. You need a boot island only for effects that must run on pages with no islands. Examples: site-wide analytics, a global keydown listener.
When another script edits a sleeping island
An island can sleep for a long time. Other scripts run meanwhile: a design-system runtime, an A/B tool, a translator. Some of them edit the DOM inside the island, a removed whitespace node, an added comment. Svelte hydrates by walking the nodes it finds, so an edited island would be thrown away and rendered again in the browser, and the click that woke it would land on a dead node.
ogygia keeps each island’s server markup from the moment it connects. On wake it hydrates with recovery off. If the walk fails and the live DOM differs from that markup, it puts the server markup back and hydrates that, which matches by construction. The island gets data-og-healed and, in dev, one warning naming it. Only if that fails too does Svelte render it client-side, as before (data-og-recovered). A heal discards whatever the other script added, so scripts should still leave <ogygia-region> subtrees alone.
Do / don’t
| Do | Don’t |
|---|---|
Set wake: 'visible' on all islands below the fold. This performance gain has no cost | Do not pass callbacks or class instances as props. They do not operate. Move the logic inside, or use a remote function or shared state |
| Keep islands small, with one purpose. A smaller island ships less JS | Do not wrap the full page in one island. That is csr = true with more steps |