Resumability
A normal signals island "hydrates": the component is re-run on the client to rebuild the reactive graph. Resumability (the Qwik model) does the opposite — the server serializes signal values into the HTML, and the client resumes interactivity without ever running the component function. Opt in with the "use resumable" directive.
computed(), multi-signal reads, and handlers (including TS-typed ones) all resume. Each island is ~1.5KB; in lazy mode, zero JS is downloaded until the first interaction.How to use it
Write an ordinary signals component with the "use resumable" directive, then render it with resumableIsland:
"use resumable";
/** @jsxImportSource @bext-stack/framework/signals */
import { signal, computed } from "@bext-stack/framework/signals";
export default function Counter(props) {
const count = signal(props.start ?? 0);
return (
<button onClick={() => count.value++}>
Count: {count.value}
</button>
);
}import { resumableIsland } from "@bext-stack/framework/signals";
import Counter from "./Counter";
// Eager — resumes on load. Ships ~1.5KB (just the lifted handlers/reads)
// plus a shared ~3.5KB runtime cached once across every island and page.
<Raw html={resumableIsland("Counter", Counter, { start: 5 })} />
// Lazy — ZERO island JS on load. The bundle downloads on the first
// interaction, resumes, and the loader replays the event.
<Raw html={resumableIsland("Counter", Counter, { start: 5 }, { lazy: true })} />Live demos
Counter — the basics. Starts at 5 (the server value); on click only the lifted handler runs and state resumes (5 → 6). The component function never runs on the client.
Toggle — a conditional text read AND a reactive attribute (aria-expanded) resuming together.
Like button — two signals (count + liked) with a handler that touches both; the pluralization is a conditional read.
Multi-signal + computed, lazy — a, b, and total = a + b. This island is lazy: its bundle is NOT downloaded on load. On the first click it downloads, resumes, then replays the event. (Network tab: /islands/ResumableSum.js only appears after the click.)
TS-typed handler — its body carries "(e: Event)" and "(e.target as HTMLInputElement)". The minimal bundle inlines it verbatim, so the compiler type-strips it to valid JS. Type and the echo updates.
How it works
The compiler lifts each handler and reactive read into self-contained factories; the server serializes signal values into the HTML:
// The compiler lifts each handler + reactive read into a tiny, self-contained
// factory map (the component function itself is NEVER shipped to the client):
export const __resume = {
rh: { "Counter@0": (scope) => { const count = scope.count; return () => count.value++; } },
rexpr: { "Counter$0": (scope) => { const count = scope.count; return count.value; } },
ccomp: {},
};
// The server serializes signal VALUES + a marker→read map into the HTML:
<bext-island data-component="Counter" data-runtime="resumable">
<script type="application/bext-resume">
{"signals":{"count":5},"computeds":[],"markers":{"0":"Counter$0"}}
</script>
<button data-bs-onclick="Counter@0">Count: <!--bs0-->5<!--/bs0--></button>
</bext-island>On the client, a shared resume runtime (~3.5KB, cached once across every island) reconstructs the signals from the serialized values, re-derives computeds, wires each DOM marker with an effect, and resolves handlers on the event — without ever re-running the component. In lazy mode nothing downloads until you interact; the first event is captured and replayed after resume.