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
| Piece | Runs in | Responsibility |
|---|---|---|
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:
First install, step by step
-
The page loads.
bit-bswup.jsreads its configuration (script attributes over theoptionsobject over the defaults) and callsregister()withupdateViaCache: 'none', so the browser never serves the worker script or its imports from the HTTP cache. -
The worker installs. It fetches
service-worker-assets.js, filters the manifest throughassetsInclude/assetsExclude, appends yourexternalAssets, and downloads the result into a fresh, version-named cache bucket. Under the defaultlaxtolerance the whole download runs inside the install event'swaitUntil, so the browser keeps the worker alive until it settles. -
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. DOWNLOAD_FINISHEDarrives withfirstInstall: true. Callingdata.reload()postsCLAIM_CLIENTSto the new worker, which claims this page so its future requests are served from the cache that was just filled.-
The claim is confirmed,
FIRST_INSTALL_CLAIMEDis raised, and Bswup callsBlazor.start(). There is no page reload - the user sees a splash turn into a running app.
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
-
Something triggers an update check: a navigation, the browser's own ~24-hour re-check,
updateInterval,updateOnVisibility, or your call toBitBswup.checkForUpdate(). -
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 raisesUPDATE_NOT_FOUND; if the check itself failed,UPDATE_CHECK_FAILED. - 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.
-
When the new version is fully staged the worker parks in the waiting state and
UPDATE_READYfires. Since v-10-6-0 nothing reloads on its own: the reload button appears and the user decides. -
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:
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:
| # | Rule | Result |
|---|---|---|
| 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. |
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'sassetsManifest.versionunless you override it withcacheVersion. - 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 withenableIntegrityCheckthat 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. prohibitedUrlsis 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-Controlmakes 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.