Skip to content

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+) replaces UseStaticFiles for 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

dotnet new blazorwasm --pwa -o MyApp

Generates: - wwwroot/manifest.jsonPWA 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):

Browser → SW → cache hit? → return cached
                          → no? → fetch from network → cache → return

Network-first (fresh API data):

Browser → SW → fetch from network → cache + return
                                  → fail? → return cached

Stale-while-revalidate (almost-fresh):

Browser → SW → return cached + fetch in bg → update cache for next time

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:

self.skipWaiting();   // in install handler

Risky — can break in-flight requests.

MapStaticAssets (.NET 9+)

app.MapStaticAssets();
// instead of:
app.UseStaticFiles();

Benefits: - Fingerprinted filenames (app.abc123.js) — long-cache safely. - Compression (gzip + brotli) auto-served. - Optimized for production.

In Razor:

<link rel="stylesheet" href="@Assets["app.css"]" />
<script src="@Assets["app.js"]"></script>

@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

event.respondWith(fetch(event.request).catch(() => caches.match(event.request)));

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.

Further reading