Islands
@sigx/ssr-islands adds islands architecture to SignalX SSR: ship a static, server-rendered page and hydrate only the interactive components, each on its own schedule, with optional per-island code splitting.
It is the first-party reference strategy pack built entirely on the pluggable
@sigx/server-renderer plugin API —
not baked into core. The base renderer is strategy-agnostic; islands is just an
SSRPlugin registered on top (installed automatically as a dependency). The base
renderer hydrates the whole tree; islands lets you hydrate just the parts marked
with client:* directives — everything else stays as cheap static HTML. Because it
ships against the same public hooks, a third party can author their own strategy
pack the same way.
It ships from the core monorepo (signalxjs/core, packages/ssr-islands) and is
released in lockstep with @sigx/server-renderer.
Why islands
Most pages are mostly static. Hydrating the entire tree ships and runs JavaScript for content that never changes. With islands you opt in to interactivity per component:
- A
client:visiblechart hydrates only when scrolled into view. - A
client:idlewidget waits for the browser to be idle. - The surrounding article ships zero client JS.
The two pieces
Using islands always involves the same pair (from the package's own quick-start):
// 1. Server — install the pack on your app
import { createSSR } from '@sigx/server-renderer';
import { islandsPlugin } from '@sigx/ssr-islands';
import { defineApp } from 'sigx';
const app = defineApp(<App />).use(islandsPlugin());
const html = await createSSR().render(app);
// 2. Client — register island components, then hydrate them
import { hydrateIslands, registerComponent } from '@sigx/ssr-islands';
import { Counter } from './components/Counter';
registerComponent('Counter', Counter);
hydrateIslands();
The client:* attributes type-check with no extra import — importing
@sigx/ssr-islands on the server or @sigx/ssr-islands/client on the client (which
the two entries above already do) augments every JSX component program-wide, the same
way core's use:* directives do:
<Counter client:visible />
<Widget client:idle />
The server plugin maps each client:* directive onto a core
boundary record, captures the
component's signal state, and injects the window.__SIGX_BOUNDARIES__ hydration
table. On the client, hydrateIslands() reads that table and schedules each island
according to its strategy.
hydrateIslands() is the whole client bootstrap. The state-restoration hooks
register themselves as a lazy plugin source, loaded with the hydration core and
resolved before the first component hydrates, so there is nothing else to wire.
(Apps that already call registerClientPlugin(islandsPlugin()) keep working —
registration dedupes by name, first wins.)
The runtime loads late
An islands page ships ~2 kB eager and executes no framework JavaScript at
load. The trigger wiring is all that runs up front; the renderer and hydration
machinery arrive by dynamic import on the first client:* strategy that actually
fires. A page whose islands are all below the fold executes nothing until the
reader scrolls — and a page of client:never boundaries never loads it at all.
The cost is one import round trip before a client:load island hydrates. To keep
that off the critical path, pass the build's manifest:
import { islandsManifest } from 'virtual:sigx-manifests';
const app = defineApp(<App />).use(islandsPlugin({ manifest: islandsManifest }));
Manifest v2 carries runtimePreload — the chunk holding the lazily-imported
executor plus its transitive imports — and the plugin emits
<link rel="modulepreload"> for it whenever a request records schedulable
islands. A page with no islands pays nothing.
interface IslandsManifestV2 {
version: 2;
islands: Record<string, { chunkUrl: string; exportName: string }>;
runtimePreload?: string[];
}
The legacy flat name → entry map is still accepted.
Hydration strategies
| Directive | Hydrates… |
|---|---|
client:load | immediately on page load |
client:idle | during browser idle time (requestIdleCallback) |
client:visible | when the element scrolls into view (IntersectionObserver) |
client:media | when a media query matches (client:media="(min-width: 768px)") |
client:interaction | on the first pointerdown / keydown / touchstart / focusin |
client:only | skip SSR entirely — empty placeholder on the server, mount fresh on the client |
See Client directives for the details of each.
Next steps
- Installation — install, dependency, and the three subpaths.
- Client directives — the six
client:*strategies. - Registry & code splitting — register components eagerly or lazily for per-island chunks.
- Plugin setup —
islandsPlugin()options, manifests, and signal-state transfer. - API reference — every export with signatures.
