Reprise (resumability)

Un îlot à signaux classique « s'hydrate » : le composant est ré-exécuté sur le client pour reconstruire le graphe réactif. La reprise (modèle Qwik) fait l'inverse — le serveur sérialise les valeurs des signaux dans le HTML, et le client reprend l'interactivité sans jamais exécuter la fonction du composant. Activée par la directive "use resumable".

Statut : livré
Bout en bout, en production sur cette page. Le composant s'exécute une fois sur le serveur, jamais sur le client ; les signaux, computed(), les lectures multi-signaux et les gestionnaires (y compris typés TS) reprennent. Chaque îlot pèse ~1,5 Ko ; en mode lazy, zéro JS n'est téléchargé avant la première interaction.

Comment l'utiliser

Écrivez un composant à signaux normal avec la directive « use resumable », puis rendez-le avec 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 })} />

Démos en direct

Compteur — la base. Démarre à 5 (valeur serveur) ; au clic, seul le gestionnaire lifté s'exécute et l'état reprend (5 → 6). La fonction du composant ne tourne jamais sur le client.



Bascule — une lecture conditionnelle ET un attribut réactif (aria-expanded) qui reprennent ensemble.



Bouton « j'aime » — deux signaux (compteur + état aimé) et un gestionnaire qui touche les deux ; la pluralisation est une lecture conditionnelle.


42 likes

Multi-signaux + computed, paresseux — a, b et total = a + b. Cet îlot est « lazy » : son bundle n'est PAS téléchargé au chargement. Au premier clic, il se télécharge, reprend, puis rejoue l'événement. (Onglet réseau : /islands/ResumableSum.js n'apparaît qu'après le clic.)


→ total = 7 (double: 14)

Gestionnaire typé TS — son corps porte « (e: Event) » et « (e.target as HTMLInputElement) ». Le mini-bundle l'intègre tel quel, donc le compilateur le dé-type en JS valide. Tapez : l'écho se met à jour.


→ you typed: “

Comment ça marche

Le compilateur lifte chaque gestionnaire et chaque lecture réactive en facteurs autonomes ; le serveur sérialise les valeurs des signaux dans le HTML :

compilé + sérialiséCode
// 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>

Au client, un runtime de reprise partagé (~3,5 Ko, mis en cache une fois pour tous les îlots) reconstruit les signaux à partir des valeurs sérialisées, recrée les computed, câble chaque marqueur DOM avec un effect, et résout les gestionnaires à l'événement — sans jamais ré-exécuter le composant. En mode lazy, rien ne se télécharge avant l'interaction ; le premier événement est capturé puis rejoué après la reprise.