@sigx/ssr-islands · Stable

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.

v0.15.3 ESM-only MIT

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:visible chart hydrates only when scrolled into view.
  • A client:idle widget 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):

TSX
// 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);
TypeScript
// 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:

TSX
<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:

TypeScript
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.

TypeScript
interface IslandsManifestV2 {
    version: 2;
    islands: Record<string, { chunkUrl: string; exportName: string }>;
    runtimePreload?: string[];
}

The legacy flat name → entry map is still accepted.

Hydration strategies#

DirectiveHydrates…
client:loadimmediately on page load
client:idleduring browser idle time (requestIdleCallback)
client:visiblewhen the element scrolls into view (IntersectionObserver)
client:mediawhen a media query matches (client:media="(min-width: 768px)")
client:interactionon the first pointerdown / keydown / touchstart / focusin
client:onlyskip SSR entirely — empty placeholder on the server, mount fresh on the client

See Client directives for the details of each.

Next steps#