bit Bswup

Preparing the app for offline use, please wait...

0%

How It Works

The mental model behind Bswup: who starts Blazor, what the service worker actually caches, how an update is staged, and which requests the worker takes over. Read this once and the whole reference stops looking like a pile of switches.

The five moving parts

PieceRuns inResponsibility
bit-bswup.js The page Registers the worker, owns the install/update handshake, decides when Blazor starts, polls for updates, and raises every lifecycle event.
bit-bswup.sw.js The service worker Precaches assets, verifies them, serves fetches, handles the SPA navigation fallback, and stages new versions. Pulled in by your service-worker.js.
bit-bswup.progress.js + BswupProgress The page The optional built-in progress UI: splash, progress bar, reload button, failure panel.
BitBswup The page The global JavaScript API: check for updates, activate a staged version, request durable storage, reset a broken client.
service-worker-assets.js Build output The manifest .NET generates at publish time: every asset URL plus its SRI hash. Bswup's entire notion of "the app" comes from this file.

Why Blazor's start is deferred

The single most important line in the setup is autostart="false" on the Blazor script. Without it, Blazor and the service worker race: Blazor starts pulling the runtime and your assemblies over the network at the same moment the worker starts precaching the very same files. The user pays for every byte twice, and there is no point in the timeline where a splash could honestly report progress - the download it is measuring is not the download the app is waiting on.

With auto-start off, Bswup owns the boot. On a first install it holds Blazor back until the cache is populated, then starts it - and every asset request Blazor makes is answered from the cache it just filled. On every later visit the cache is already warm, so Bswup starts Blazor immediately and the app boots offline-fast.

The two lifecycles

Nearly every question about Bswup's behavior resolves to "is this a first install or an update?" They are genuinely different flows, and most options behave differently in each:

The Bswup first-install and update lifecycles Two parallel sequences. First install: the page loads with Blazor deferred; Bswup registers the worker, which precaches assets; DOWNLOAD_PROGRESS drives the splash; DOWNLOAD_FINISHED arrives with firstInstall true; reload() runs the CLAIM_CLIENTS handshake; FIRST_INSTALL_CLAIMED fires and Blazor starts. Update: an update check finds a new worker; UPDATE_FOUND is raised and the new worker installs; the download runs behind the running app; UPDATE_READY signals a fully staged version; the user accepts and SKIP_WAITING activates it; ACTIVATE prunes old caches and every open tab reloads onto the new version. First install the app is not running yet Update the app is already running Page loads - Blazor start is deferred register() - the worker installs DOWNLOAD_PROGRESS drives the splash DOWNLOAD_FINISHED (firstInstall: true) reload() - CLAIM_CLIENTS handshake Blazor starts - no page reload Check: navigation / timer / focus / API UPDATE_FOUND - new worker installs Download runs behind the live app UPDATE_READY - version fully staged User accepts - SKIP_WAITING ACTIVATE - prune caches - tabs reload

First install, step by step

  1. The page loads. bit-bswup.js reads its configuration (script attributes over the options object over the defaults) and calls register() with updateViaCache: 'none', so the browser never serves the worker script or its imports from the HTTP cache.
  2. The worker installs. It fetches service-worker-assets.js, filters the manifest through assetsInclude / assetsExclude, appends your externalAssets, and downloads the result into a fresh, version-named cache bucket. Under the default lax tolerance the whole download runs inside the install event's waitUntil, so the browser keeps the worker alive until it settles.
  3. Every finished asset raises DOWNLOAD_PROGRESS. This is what fills the splash bar - and the reason the percentage is honest: it counts the same bytes the app is waiting on.
  4. DOWNLOAD_FINISHED arrives with firstInstall: true. Calling data.reload() posts CLAIM_CLIENTS to the new worker, which claims this page so its future requests are served from the cache that was just filled.
  5. The claim is confirmed, FIRST_INSTALL_CLAIMED is raised, and Bswup calls Blazor.start(). There is no page reload - the user sees a splash turn into a running app.
If any of that goes wrong, the app still boots
A fatal install failure, or stallTimeout seconds of total worker silence, makes Bswup start Blazor directly from the network. The page runs uncontrolled - exactly as if no service worker existed - and the install is retried on the next load. A first visit never ends in a frozen splash.

Updates, step by step

  1. Something triggers an update check: a navigation, the browser's own ~24-hour re-check, updateInterval, updateOnVisibility, or your call to BitBswup.checkForUpdate().
  2. The browser re-fetches service-worker.js. If a byte changed anywhere in the import chain, it treats the worker as new and installs it - UPDATE_FOUND. If nothing changed, Bswup raises UPDATE_NOT_FOUND; if the check itself failed, UPDATE_CHECK_FAILED.
  3. The new worker precaches the new version into its own bucket, behind the running app. The old worker keeps serving every request from the old bucket, so the running app never sees a half-migrated cache. The splash is deliberately not shown.
  4. When the new version is fully staged the worker parks in the waiting state and UPDATE_READY fires. Since v-10-6-0 nothing reloads on its own: the reload button appears and the user decides.
  5. Accepting posts SKIP_WAITING. The new worker activates, prunes the stale buckets, and the tab reloads onto the new version.

When an update reloads your tabs

A finished update applies itself and reloads the tab, because AutoReload defaults to true. An unprompted reload is not a cosmetic annoyance though - it discards whatever the user had typed, scrolled to, or half-completed - so set AutoReload="false" to have the update announce itself through the reload button and let the user accept it. But the opposite failure is worse and much less obvious, so Bswup handles it for you:

The stale-tab hazard
A service worker is single-instance per origin, so accepting an update in one tab activates the new version for every tab. Any other tab is now running old application code while its asset requests are answered from the new version's cache - mismatched boot config and assembly hashes, which surfaces as bizarre runtime errors rather than an honest "please reload". Bswup has the new worker claim all clients, and every other tab reloads itself via controllerchange. One tab decides; all tabs stay consistent.

The request pipeline

Once the worker controls the page, every request passes through it. The order below is the order the worker actually evaluates - the first rule that matches wins:

#RuleResult
1 URL matches prohibitedUrls 403 Forbidden with a fixed text/plain body, for every method.
2 Not a GET, or the URL matches serverHandledUrls Left alone entirely - the browser handles it as if no worker existed.
3 A navigation, not server-rendered, forcePrerender off Served the app shell (defaultUrl) - unless the navigated URL is itself a concrete managed asset, in which case that asset is served.
4 The URL resolves to a managed asset Served from cache. A miss goes to the network and is written back, so an asset skipped by a lax install (or evicted) becomes offline-capable after its first successful fetch.
5 Anything else Left alone. Your API calls, third-party beacons, and anything outside the manifest never enter the worker's logic.
Decide first, commit second
Rules 2 and 5 matter more than they look. Calling respondWith() is a commitment: the worker then owns the response and the browser will not fall back to its own network stack, so one rejected fetch inside the worker becomes a hard network error for the page. Bswup routes every request synchronously and only takes over the ones it manages - which is exactly why a network blip on an unrelated request can never freeze your app behind a stalled progress bar.

The caching model

  • One bucket per version, per scope. Buckets are named bit-bswup:<scope-path> - <version>. The scope qualifier keeps several Bswup apps on one origin from evicting each other's caches; the version comes from Blazor's assetsManifest.version unless you override it with cacheVersion.
  • The version rotates only when an asset hash changes. A publish that changes nothing reuses the same bucket, so users re-download nothing.
  • Old buckets are pruned on activation, never while an update is staged or staging - a prune can't race a newer install's freshly written bucket.
  • Each asset is cache-busted by its own hash (?v=), and with enableIntegrityCheck that hash is also enforced as Subresource Integrity - a corrupted or tampered asset fails to install rather than silently poisoning the cache.
  • Hash-less assets carry no version to diff, so they are re-downloaded once per update rather than per request (turn that off with disableHashlessAssetsUpdate).

What Bswup deliberately does not do

  • It is not a runtime data cache. Bswup caches your application - the shell, runtime, assemblies, and static assets. API responses and user data are yours to handle; route them past the worker with serverHandledUrls.
  • prohibitedUrls is not a security boundary. It is enforced only inside the worker, which is bypassed on the very first visit, on a hard reload, and by anything talking to your server directly. Enforce access control on the server.
  • It does not replace your HTTP caching. The two compose: long-lived Cache-Control makes repeat visits fast, and the worker's hash-based versioning means a stale HTTP cache is never a correctness problem. The exception is the worker scripts themselves - see the hosting checklist.
Watch it happen
Everything on this page is observable on this very site. The Live Playground streams each event as it is raised, lists the live cache buckets, and wires the JavaScript API to buttons.