Turning a Blazor WebAssembly App into a PWA: How the Built‑In Service Worker Works
Learn how Blazor WebAssembly’s ‑pwa flag scaffolds a service worker, what assets get cached, and how to verify offline functionality. A step‑by‑step example shows bundle impact, trade‑offs, and how to customize caching.
30 Jan 2026, 06:36 UTC

Problem: Need a Blazor app that works without a network
Modern users expect instant access to web content, even on flaky connections. For a Blazor WebAssembly (WASM) app, that means the client‑side bundle must be available from the browser’s cache. Without a service worker, the browser will always try to fetch the entire app from the network, causing a full reload on every visit.
Thesis: Blazor’s ‑pwa flag gives you a ready‑made service worker, but it’s a simple cache‑first strategy that only covers static assets.
When you create a project with dotnet new blazorwasm -pwa, the template adds a manifest and a sw.js file. The service worker pre‑caches the entire static asset bundle and serves those files from the cache on subsequent loads. This gives you instant startup on the first offline visit, but it does not handle dynamic API data or complex caching rules out of the box.
1. Scaffolding a PWA Project
- Run
dotnet new blazorwasm -pwa -o MyPwaAppin a terminal where you have the .NET SDK installed. Permissions: No special permissions are required; the command creates a new folderMyPwaAppwith the necessary files. - Navigate into the folder:
cd MyPwaApp. - Build and run the app locally:
dotnet run. The app will be served athttps://localhost:5001by default.
2. What the Auto‑Generated Service Worker Does
The sw.js file looks like this:
/* sw.js – generated by the Blazor PWA template */
const CACHE_NAME = 'blazor-cache-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/_framework/blazor.webassembly.js',
'/_framework/blazor.boot.json',
'/_framework/wasm/mono.wasm',
'/_framework/wasm/mono.dll',
// ... all other static files
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS_TO_CACHE))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => response || fetch(event.request))
);
});
The install step pre‑caches every file listed in ASSETS_TO_CACHE. The fetch handler implements a cache‑first strategy: it serves the cached file if available, otherwise it falls back to a network request. This is sufficient for static assets but not for dynamic data.
3. Verifying Offline Operation
- Open the running app in Chrome (or Edge) and press F12 to open DevTools.
- Navigate to the Application tab, then to Service Workers. You should see a worker registered at
sw.jswith a status of “Activated”. - Switch to the Manifest tab to confirm the app appears as a PWA candidate.
- Open the Network tab, check the Offline box, and reload the page. All static assets should load from the cache (displayed as
from memory cacheorfrom disk cache). - Verify that the UI still functions (e.g., navigation, component rendering). Dynamic API calls will fail unless you implement additional caching.
4. Customizing Caching and Trade‑Offs
While the default service worker is quick to set up, it can bloat the initial bundle by a few kilobytes and may not suit all use cases. Here’s how to tweak it:
- Reduce bundle size: Remove unnecessary files from
ASSETS_TO_CACHEor use a build step to strip debug symbols. - Cache API responses: Add a new fetch handler for your API endpoints and store the responses in a separate cache.
- Implement a stale‑while‑revalidate strategy: Serve the cached response immediately, then fetch the latest version in the background and update the cache.
Example: Add API caching to sw.js:
const API_CACHE = 'api-cache-v1';
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.open(API_CACHE).then(cache => {
return cache.match(event.request).then(cached => {
const network = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return cached || network;
});
})
);
}
});
Be aware that caching dynamic data can lead to stale content if not refreshed correctly, and it increases storage usage on the client.
Limitations and Practical Checks
- Service workers are not supported in Safari on iOS; users on those browsers will not get offline functionality.
- Only static assets are cached by default; API calls require manual caching.
- Adding a service worker increases the initial payload, which can hurt first‑load performance on slow networks.
To check the impact, run dotnet publish -c Release, then compare the wwwroot/_framework folder size before and after adding the PWA flag.
Actionable Takeaway
For most Blazor WASM apps that need simple offline support, the -pwa flag is a zero‑cost solution. Create the project, run it, verify the service worker in DevTools, and you’re ready to install the app on a device. If you need more advanced caching (e.g., for API data or large media assets), copy the generated sw.js into a wwwroot/service-worker.js file and extend the fetch handler as shown above. Always test in the target browsers and monitor the cache size to keep the user experience smooth.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.