20. Browser (WebAssembly) target

Goal

Why this matters

Prerequisites

1. Project structure and setup

Install wasm-tools workload:

sudo dotnet workload install wasm-tools

A multi-target solution has:

Avalonia template (dotnet new avalonia.app --multiplatform) can create the browser head for you. MyApp.Browser references Avalonia.Browser, which wraps the WebAssembly host (BrowserAppBuilder, BrowserSingleViewLifetime, BrowserNativeControlHost).

When adding the head manually, target net8.0-browserwasm, configure <WasmMainJSPath>wwwroot/main.js</WasmMainJSPath>, and keep trimming hints (e.g., <InvariantGlobalization>true</InvariantGlobalization>). Browser heads use the NativeAOT toolchain; Release builds can set <PublishAot>true</PublishAot> for faster startup and smaller payloads.

2. Start the browser app

StartBrowserAppAsync attaches Avalonia to a DOM element by ID.

using Avalonia;
using Avalonia.Browser;

internal sealed class Program
{
    private static AppBuilder BuildAvaloniaApp()
        => AppBuilder.Configure<App>()
            .UsePlatformDetect()
            .LogToTrace();

    public static Task Main(string[] args)
        => BuildAvaloniaApp()
            .StartBrowserAppAsync("out");
}

Ensure host HTML contains <div id="out"></div>.

For advanced embedding, use BrowserAppBuilder directly:

await BrowserAppBuilder.Configure<App>()
    .SetupBrowserAppAsync(options =>
    {
        options.MainAssembly = typeof(App).Assembly;
        options.AppBuilder = AppBuilder.Configure<App>().LogToTrace();
        options.Selector = "#out";
    });

SetupBrowserAppAsync lets you delay instantiation (wait for configuration, auth, etc.) or mount multiple roots in different DOM nodes.

3. Single view lifetime

Browser uses ISingleViewApplicationLifetime (same as mobile). Configure in App.OnFrameworkInitializationCompleted:

public override void OnFrameworkInitializationCompleted()
{
    if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
        singleView.MainView = new ShellView { DataContext = new ShellViewModel() };
    else if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        desktop.MainWindow = new MainWindow { DataContext = new ShellViewModel() };

    base.OnFrameworkInitializationCompleted();
}

Navigation patterns from Chapter 19 apply (content control with back stack).

4. Rendering options

Configure BrowserPlatformOptions to choose rendering mode and polyfills.

await BuildAvaloniaApp().StartBrowserAppAsync(
    "out",
    new BrowserPlatformOptions
    {
        RenderingMode = new[]
        {
            BrowserRenderingMode.WebGL2,
            BrowserRenderingMode.WebGL1,
            BrowserRenderingMode.Software2D
        },
        RegisterAvaloniaServiceWorker = true,
        AvaloniaServiceWorkerScope = "/",
        PreferFileDialogPolyfill = false,
        PreferManagedThreadDispatcher = true
    });

5. Storage and file dialogs

IStorageProvider uses the File System Access API when available; otherwise a polyfill (service worker + download anchor) handles saves.

Limitations:

Example save using polyfill-friendly code (Chapter 16 shows full pattern). Test with/without service worker to ensure both paths work.

6. Clipboard & drag-drop

Clipboard operations require user gestures and may only support text formats.

Drag/drop from browser to app is supported, but dragging files out of the app is limited by browser APIs.

7. Networking & CORS

8. JavaScript interop

Call JS via window.JSObject or JSRuntime helpers (Avalonia.Browser exposes interop helpers). Example:

using Avalonia.Browser.Interop;

await JSRuntime.InvokeVoidAsync("console.log", "Hello from Avalonia");

Use interop to integrate with existing web components or to access Web APIs not wrapped by Avalonia.

To host native DOM content inside Avalonia, use BrowserNativeControlHost with a JSObjectControlHandle:

var handle = await JSRuntime.CreateControlHandleAsync("div", new { @class = "web-frame" });
var host = new BrowserNativeControlHost { Handle = handle };

This enables hybrid UI scenarios (rich HTML editors, video elements) while keeping sizing/layout under Avalonia control.

9. Hosting in Blazor (optional)

Avalonia.Browser.Blazor lets you embed Avalonia controls in a Blazor app. Example sample: ControlCatalog.Browser.Blazor. Use when you need Blazor's routing/layout but Avalonia UI inside components.

10. Hosting strategies

During development, dotnet run on the browser head launches a Kestrel server with live reload and proxies console logs back to the terminal.

11. Debugging and diagnostics

12. Performance tips

13. Deployment

Publish the browser head:

cd MyApp.Browser
# Debug
dotnet run
# Release bundle
dotnet publish -c Release

Output under bin/Release/net8.0/browser-wasm/AppBundle. Serve via static web server (ASP.NET, Node, Nginx, GitHub Pages). Ensure service worker scope matches hosting path.

Remember to enable compression (Brotli) for faster load times.

14. Platform limitations

Feature Browser behavior
Windows/Dialogs Single view only; no OS windows, tray icons, native menus
File system User-selection only via pickers; no arbitrary file access
Threading Multi-threaded WASM requires server headers (COOP/COEP) and browser support
Clipboard Requires user gesture; limited formats
Notifications Use Web Notifications API via JS interop
Storage LocalStorage/IndexedDB via JS interop for persistence

Design for progressive enhancement: provide alternative flows if feature unsupported.

15. Practice exercises

  1. Add a browser head and run the app in Chrome/Firefox, verifying rendering fallbacks.
  2. Implement file export via IStorageProvider and test save polyfill with service worker enabled/disabled.
  3. Add logging to report BrowserPlatformOptions.RenderingMode and ActualTransparencyLevel (should be None).
  4. Integrate a JavaScript API (e.g., Web Notifications) via interop and show a notification after user action.
  5. Publish a release build and deploy to a static host (GitHub Pages or local web server), verifying service worker scope and COOP/COEP headers.
  6. Use wasm-tools wasm-strip (or wasm-opt) to inspect bundle size before/after trimming and record the change.

Look under the hood (source bookmarks)

Check yourself

What's next