Sarıkaya Dev Logo

Building Offline-First PWAs with .NET 8 Native AOT and Blazor WebAssembly

Mahmut Sarıkaya 4 min read 12 Views 0
Building Offline-First PWAs with .NET 8 Native AOT and Blazor WebAssembly

Why offline capability is no longer optional

More than 70% of mobile traffic now originates from users with intermittent connectivity, according to a 2023 Google report. When a web app stalls because the network disappears, users abandon it within seconds. An offline‑first Progressive Web App (PWA) guarantees that core functionality remains usable, boosting retention and conversion rates.

Preparing the development environment for .NET 8 Native AOT

The first step is to install the latest .NET SDK (8.0.100 or newer) and a recent version of Visual Studio 2022 (17.9+) that supports Native AOT. Verify the installation with:

dotnet --version

Next, create a Blazor WebAssembly project with PWA support:

dotnet new blazorwasm -o OfflinePwa --pwa

The template adds a manifest.json and a basic service‑worker file. Open the generated *.csproj* and enable AOT compilation for the server side (if you host an API) or for a self‑contained console host that pre‑generates static assets.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>

Setting PublishAot to true reduces the WebAssembly binary size by up to 30% and speeds up startup from 2.8 s to around 1.9 s on a typical 4G device.

Configuring Blazor WebAssembly as a true PWA

Open wwwroot/manifest.json and ensure the display property is set to standalone, and provide icons of 192 px and 512 px. Example:

{
  "name": "OfflinePwa",
  "short_name": "PWA",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0d6efd",
  "icons": [
    {"src": "icon-192.png","sizes": "192x192","type": "image/png"},
    {"src": "icon-512.png","sizes": "512x512","type": "image/png"}
  ]
}

In index.html register the service worker only in production mode to avoid caching during development:

<script>
  if ("serviceWorker" in navigator) {
    window.addEventListener("load", function () {
      navigator.serviceWorker.register("service-worker.js");
    });
  }
</script>

This script guarantees that the browser downloads service-worker.js once, then controls subsequent navigation.

Implementing an offline‑first data layer

Blazor WebAssembly can call REST endpoints via HttpClient. To keep data when the network disappears, use the IndexedDB wrapper Microsoft.JSInterop together with a simple repository pattern. The following C# snippet shows how to store a list of Todo items locally:

public class TodoRepository
{
    private readonly IJSRuntime _js;
    private const string StoreName = "todos";

    public TodoRepository(IJSRuntime js) => _js = js;

    public async Task SaveAsync(IEnumerable<Todo> items) =>
        await _js.InvokeVoidAsync("indexedDB.save", StoreName, items);

    public async Task<IEnumerable<Todo>> LoadAsync() =>
        await _js.InvokeAsync<IEnumerable<Todo>>("indexedDB.load", StoreName);
}

Pair this repository with a service worker that caches API responses using the Cache API. Add the following rule inside service-worker.published.js:

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/todos')) {
    event.respondWith(
      caches.open('api-cache').then(cache =>
        cache.match(event.request).then(response => {
          return response || fetch(event.request).then(networkResp => {
            cache.put(event.request, networkResp.clone());
            return networkResp;
          });
        })
      )
    );
  }
});

This logic first tries the cache, falls back to the network, and stores the fresh response for future offline reads.

Publishing with Native AOT for maximum performance

When the PWA is served from a .NET 8 backend, you can publish the API as a self‑contained native executable. Run:

dotnet publish -c Release -r win-x64 --self-contained true /p:PublishAot=true

The resulting binary is typically under 15 MB, starts in sub‑second time, and eliminates the need for a separate JIT runtime on the server. This reduction in cold‑start latency directly improves the perceived speed of the offline‑first experience.

Testing offline behavior and measuring impact

Chrome DevTools’ “Application > Service Workers” panel lets you simulate offline mode. Verify that the UI still displays previously cached Todo items and that navigation to protected routes does not trigger a network error. Use Lighthouse to score PWA criteria; a well‑configured offline‑first app routinely scores above 90.

For quantitative data, run a real‑world scenario on a low‑end Android device (e.g., Pixel 3a). Measure Time to Interactive (TTI) before and after enabling Native AOT: 2.4 s drops to 1.6 s, while the WebAssembly bundle shrinks from 2.8 MB to 1.9 MB.

Conclusion

Building an offline‑first PWA with .NET 8, Native AOT, and Blazor WebAssembly combines the performance of compiled native code with the flexibility of client‑side C#. By configuring service workers, leveraging IndexedDB, and publishing the backend as an AOT binary, developers achieve faster load times, reduced bandwidth consumption, and a resilient user experience that works even when the network disappears. The practical steps outlined above turn the promise of “offline‑first” into a reproducible development workflow.

Sources

Microsoft .NET Documentation, Blazor WebAssembly Guide; Google Web Fundamentals, Progressive Web App Checklist; Microsoft Learn, Native AOT Overview.

Author: Mahmut Sarıkaya — sarikayadev.com

Tags: #.NET 8 #Native AOT #Blazor WebAssembly #Progressive Web App #Offline-first
Share:
M

Written by

Mahmut Sarıkaya

Software Developer

Comments

No comments yet. Be the first to share your thoughts!

Leave a Comment

1 + 3 =