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.

Status: shipped
End-to-end, in production on this page. The component runs once on the server, never on the client; signals, 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:

Counter.tsxTSX
"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>
  );
}
page.tsxTSX
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.


42 likes

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


→ total = 7 (double: 14)

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.


→ you typed: “

How it works

The compiler lifts each handler and reactive read into self-contained factories; the server serializes signal values into the HTML:

compiled + serializedCode
// 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.