Guide · @staticflux/pwa · @staticflux/hydrate

PWA & hydration

Instant boot from three directions: a service worker built from the chunk manifest, islands that hydrate prerendered HTML on demand, and state snapshots restored before the network answers.

Service worker and updates

@staticflux/pwa/sw runs inside the service worker: an atomic precache of the critical tier from sf-manifest.json (serviceWorkerConfigFromManifest), a warm cache that likely / speculative warmups fill at low priority, cache strategies per rule (Wasm, locale catalogs, lazy assets), network-first navigations with an app-shell fallback, and optional plugins (background sync of idempotent mutations, push, share target, COOP/COEP injection for static hosts).

On the page, createPwaUpdater({ env, scriptUrl, frameworkMajor, reinitialize, prompt }) registers the worker and runs the update flow: a new version → a messaging prompt → skip-waiting only on accept (chunk versions never mix) → supervisor.reinitialize() without a reload. A framework-major change or an unknown version falls back to a full reload — the explicit escape hatch.

TypeScript
const updater = createPwaUpdater({
  env: browserPwaPageEnv(),
  scriptUrl: '/sw.js',
  frameworkMajor: 0,
  reinitialize: { reinitialize: () => supervisor.reinitialize() },   // ReinitializePort
  prompt: { offer: (info, signal) => askToUpdate(info, signal) },     // UpdatePromptPort → 'accept' | 'dismiss'
});
await updater.register();

Launch data — links, files, share targets, notification clicks — arrives as LaunchIntents through a port, as data only (prefill, never executed). defineWebAppManifest validates the web app manifest at build time.

Islands over prerendered HTML

this site’s prerendered HTML
<!-- build time: renderIslandMarker({ chunk: 'island:code', hydrate: 'interaction', html }) -->
<sf-island chunk="island:code" hydrate="interaction">
  <figure class="code">…<button data-copy>Copy</button>…</figure>
</sf-island>
site/src/client (abridged)
// the island chunk: a named hydrate export (or defineIsland({ props, hydrate }))
export const hydrate = defineIsland({
  async hydrate(element, { injector, scope, signal }) {
    const button = element.querySelector('[data-copy]')!;
    scope.listen(button, 'click', () => copy(element));     // everything in the island scope
    return ok();
  },
}).hydrate;

// boot: one hydrator for the page, islands load through the chunk loader (gated)
createIslandHydrator({ loader, telemetry }).start(document.body, { injector: app.injector });
  • Strategies: visible (default), idle, interaction (preloaded on hover / focus, hydrated on first input), eager (requires a reason), never.
  • Event replay: clicks, input and submits before hydration are queued and replayed on the original targets afterwards.
  • The island gate and loader.check run before any fetch; a denied island is skipped and its chunk never requested.
  • Static markup is the first render; hydration is “interactive” — both are feature marks, and a size mismatch after hydration is reported as a layout-shift risk.

State rehydration

createSnapshotRegistry persists store and resource snapshots to an AsyncStore and restores them at boot before the network — versioned, identity-bound (opaque hashes), budgeted and time-boxed (default 200 ms). Restored data is tagged snapshot and revalidated (SWR); a restore never bumps the epoch, so old-epoch responses are still discarded. Snapshot failures never fail boot.

AGENTS.md on GitHub: @staticflux/pwa · @staticflux/hydrate · @staticflux/loader