PWA & Static Assets
Key Points
- Blazor WASM can be a PWA: installable, offline-capable. Service worker caches the app + API responses (with care).
- Manifest.json + service worker = PWA. Project template:
dotnet new blazorwasm --pwa. MapStaticAssets(.NET 9+) replacesUseStaticFilesfor compressed, fingerprinted assets.- Caching strategy for service worker: cache-first (app shell), network-first (API), stale-while-revalidate (semi-fresh).
- Updates: service worker waits for tab close to activate new version. Force update flow tricky.
Concepts (deep dive)
PWA basics
Generates: - wwwroot/manifest.json — PWA manifest (name, icons, colors). - wwwroot/service-worker.js — production worker. - wwwroot/service-worker.published.js — for dotnet publish builds.
manifest.json
{
"name": "MyApp",
"short_name": "MyApp",
"start_url": "./",
"display": "standalone",
"icons": [
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "icon-512.png", "sizes": "512x512", "type": "image/png" }
],
"background_color": "#ffffff",
"theme_color": "#03a9f4"
}
Allows "Install" prompt in Chrome/Edge.
Service worker
// service-worker.published.js
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
async function onInstall(event) {
const assetsRequests = self.assetsManifest.assets
.map(asset => new Request(asset.url, { integrity: asset.hash }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onFetch(event) {
if (event.request.method !== 'GET') return fetch(event.request);
const cached = await caches.match(event.request);
return cached ?? fetch(event.request);
}
Build emits service-worker-assets.js listing all the files + integrity hashes.
Caching strategies
Cache-first (app shell):
Network-first (fresh API data):
Stale-while-revalidate (almost-fresh):
Update flow
User opens app v1.
SW v2 deploys.
User refreshes → SW v2 installs but waits.
User closes all tabs.
Next open → SW v2 activates.
Forcing immediate activation:
Risky — can break in-flight requests.
MapStaticAssets (.NET 9+)
Benefits: - Fingerprinted filenames (app.abc123.js) — long-cache safely. - Compression (gzip + brotli) auto-served. - Optimized for production.
In Razor:
@Assets resolves to fingerprinted name in production.
Combining with PWA
The PWA service worker caches the fingerprinted assets. Long cache lifetime safe (filenames change with content).
Offline detection
@inject IJSRuntime JS
@code {
bool _online = true;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("registerOnlineHandler", DotNetObjectReference.Create(this));
}
}
[JSInvokable]
public void OnConnectionChange(bool online)
{
_online = online;
StateHasChanged();
}
}
window.registerOnlineHandler = (ref) => {
window.addEventListener('online', () => ref.invokeMethodAsync('OnConnectionChange', true));
window.addEventListener('offline', () => ref.invokeMethodAsync('OnConnectionChange', false));
};
Local data persistence
WASM has access to: - localStorage / sessionStorage (small; ~10 MB). - IndexedDB (large; structured). - Cache API (request/response pairs). - OPFS (Origin Private File System) for file-like storage.
Libraries: - Blazored.LocalStorage - Blazored.SessionStorage - Microsoft.AspNetCore.Components.QuickGrid (for stateful tables)
Push notifications
Service worker + Web Push API. Requires user permission. Backend: web-push library to send.
Background sync
Queue ops while offline; replay when back online. sync event in service worker.
App update detection
navigator.serviceWorker.register('./service-worker.js').then(reg => {
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
showUpdatePrompt();
}
});
});
});
Show "Update available; reload" UI.
When PWA, when not
PWA fits: - Public consumer apps (forms, dashboards). - Tools used offline (notes, drawing). - Mobile-friendly experiences.
PWA doesn't fit: - Internal LOB apps with constant network. - Apps requiring real-time data freshness. - Server-side state-heavy apps.
Limitations
- Service worker scope: directory and below.
- HTTPS required.
- Install prompt requires manifest + valid icons.
- Cache invalidation hard.
Code: correct vs wrong
❌ Wrong: PWA without HTTPS
PWAs require HTTPS. Local dev at http://localhost works (special exception).
❌ Wrong: cache-everything API responses
// Cached forever; data goes stale
event.respondWith(caches.match(event.request).then(r => r || fetch(event.request)));
✅ Correct: network-first for API
Design patterns for this topic
Pattern 1 — "MapStaticAssets in .NET 9+"
- Intent: fingerprinted; compressed; long-cache.
Pattern 2 — "Cache-first for shell; network-first for API"
- Intent: offline app + fresh data.
Pattern 3 — "Update prompt UI"
- Intent: inform user of new version.
Pattern 4 — "Offline detection"
- Intent: UX feedback.
Pattern 5 — "IndexedDB for structured offline data"
- Intent: large local storage.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| PWA | Installable; offline | Update complexity |
| Service worker | Cache control | Lifecycle quirks |
| MapStaticAssets | Optimal serving | .NET 9+ |
When to use / when to avoid
- Use PWA for consumer / offline.
- Use MapStaticAssets in .NET 9+ apps.
- Avoid PWA for always-online enterprise apps.
- Avoid cache-everything strategy.
Interview Q&A
Q1. PWA requirements? HTTPS, manifest.json, service worker, icons.
Q2. Service worker lifecycle? register → install → activate → fetch → update.
Q3. Cache strategies? Cache-first, network-first, stale-while-revalidate.
Q4. skipWaiting? Activate new service worker without waiting for tab close. Risky.
Q5. MapStaticAssets vs UseStaticFiles? MapStaticAssets fingerprints + compresses. Production-optimized.
Q6. Offline detection? window.online/offline events; navigator.onLine.
Q7. IndexedDB vs localStorage? Local: small KV. IndexedDB: large structured.
Q8. PWA fit? Public/consumer; offline-friendly; mobile.
Q9. Update prompt? Listen for updatefound; show UI; reload.
Q10. Push notifications? Service worker + Web Push API. User permission.
Q11. Background sync? Queue ops offline; replay online.
Q12. Cache invalidation? Versioned cache name. Old caches purged on activate.
Gotchas / common mistakes
- ⚠️ Cache-everything-API — stale data forever.
- ⚠️ No update UI — users on old version forever.
- ⚠️ HTTPS missing in production.
- ⚠️ Service worker scope wrong — limited paths.
- ⚠️ Forgetting fingerprint cache-busting — old assets served.