Blazor WebAssembly represents Microsoft’s bold vision for web development: write client-side applications in C# instead of JavaScript, running directly in the browser via WebAssembly. In early 2020, with the preview maturing rapidly, enterprises began evaluating Blazor for production scenarios. This comprehensive guide explores architecture patterns, performance optimization, and real-world deployment strategies for Blazor WebAssembly applications.
Understanding the Blazor WebAssembly Architecture
Unlike Blazor Server (which runs on the server and sends UI updates via SignalR), Blazor WebAssembly runs entirely in the browser. The .NET runtime is compiled to WebAssembly, and your C# code executes client-side with no persistent server connection required.
graph TB
subgraph Browser ["Browser"]
WASM[".NET WASM Runtime"]
App["Blazor App DLLs"]
DOM["DOM"]
end
subgraph Server ["Backend API"]
API["REST/gRPC API"]
DB["Database"]
end
User[User] --> DOM
DOM --> WASM
WASM --> App
App --> API
API --> DB
style WASM fill:#E1F5FE,stroke:#0277BD
style App fill:#C8E6C9,stroke:#2E7D32
The Download Size Challenge
The primary concern with Blazor WebAssembly is initial download size. The .NET runtime, base class libraries, and your application DLLs must all be downloaded before the app can execute. A minimal Blazor app downloads approximately 2-3 MB (compressed). Strategies to mitigate this include:
- Lazy Loading: Load assemblies on-demand as users navigate to specific features
- Trimming: Remove unused code during publish (IL Linker)
- Compression: Enable Brotli compression (significantly reduces transfer size)
- Caching: Aggressive browser caching with content hashing
Project Structure and Setup
Creating a production-ready Blazor WebAssembly project requires thoughtful organization. Here’s a recommended structure:
# Create the solution
dotnet new blazorwasm -o MyBlazorApp --hosted
# Project structure
MyBlazorApp/
├── Client/ # Blazor WebAssembly project
│ ├── Pages/ # Routable components
│ ├── Shared/ # Shared UI components
│ ├── Services/ # Client-side services
│ └── wwwroot/ # Static assets
├── Server/ # ASP.NET Core host
│ ├── Controllers/ # API endpoints
│ └── Hubs/ # SignalR hubs (optional)
└── Shared/ # Shared models and contracts
└── Models/ # DTOs used by both client and server
Component Development Patterns
Blazor components (.razor files) combine HTML markup with C# code. Understanding lifecycle methods is crucial for building performant components:
@page "/users/{UserId:int}"
@inject IUserService UserService
@inject NavigationManager Navigation
@implements IDisposable
@if (_loading)
{
<LoadingSpinner />
}
else if (_user is null)
{
<NotFound Message="User not found" />
}
else
{
<div class="user-profile">
<h1>@_user.FullName</h1>
<p>Email: @_user.Email</p>
<p>Member since: @_user.CreatedAt.ToShortDateString()</p>
<button class="btn btn-primary" @onclick="EditUser">
Edit Profile
</button>
</div>
}
@code {
[Parameter]
public int UserId { get; set; }
private User? _user;
private bool _loading = true;
private CancellationTokenSource _cts = new();
protected override async Task OnInitializedAsync()
{
await LoadUserAsync();
}
protected override async Task OnParametersSetAsync()
{
// Called when UserId parameter changes (e.g., navigation)
await LoadUserAsync();
}
private async Task LoadUserAsync()
{
_loading = true;
StateHasChanged();
try
{
_user = await UserService.GetUserAsync(UserId, _cts.Token);
}
catch (OperationCanceledException)
{
// Component disposed during load
}
catch (Exception ex)
{
Console.WriteLine($"Error loading user: {ex.Message}");
}
finally
{
_loading = false;
StateHasChanged();
}
}
private void EditUser()
{
Navigation.NavigateTo($"/users/{UserId}/edit");
}
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}
State Management Strategies
As Blazor applications grow, managing state becomes critical. Several patterns are available:
1. Cascading Parameters
For simple hierarchical state (like themes or authentication), use CascadingValue:
// App.razor
<CascadingValue Value="@currentTheme">
<Router AppAssembly="@typeof(App).Assembly">
...
</Router>
</CascadingValue>
// Any child component
[CascadingParameter]
public Theme CurrentTheme { get; set; }
2. Scoped Services (DI)
For cross-component state, use a scoped service with events:
public class AppState
{
public User? CurrentUser { get; private set; }
public event Action? OnChange;
public void SetUser(User user)
{
CurrentUser = user;
NotifyStateChanged();
}
public void ClearUser()
{
CurrentUser = null;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
// Register in Program.cs
builder.Services.AddScoped<AppState>();
// Use in component
@inject AppState AppState
@implements IDisposable
@code {
protected override void OnInitialized()
{
AppState.OnChange += StateHasChanged;
}
public void Dispose()
{
AppState.OnChange -= StateHasChanged;
}
}
3. Fluxor (Redux Pattern)
For complex applications, consider Fluxor—a Redux-style state management library for Blazor:
// State
public record CounterState(int Count);
// Actions
public record IncrementCounterAction();
public record DecrementCounterAction();
// Reducer
public static class CounterReducers
{
[ReducerMethod]
public static CounterState Reduce(CounterState state, IncrementCounterAction action)
=> state with { Count = state.Count + 1 };
}
// Usage in component
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IState<CounterState> CounterState
@inject IDispatcher Dispatcher
<p>Current count: @CounterState.Value.Count</p>
<button @onclick="Increment">+1</button>
@code {
private void Increment() => Dispatcher.Dispatch(new IncrementCounterAction());
}
HTTP Communication and API Integration
Blazor uses HttpClient for API calls. Configure it properly for production:
// Program.cs - Configure typed HTTP clients
builder.Services.AddHttpClient<IUserService, UserService>(client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<AuthorizationMessageHandler>();
// Service implementation
public class UserService : IUserService
{
private readonly HttpClient _http;
private readonly ILogger<UserService> _logger;
public UserService(HttpClient http, ILogger<UserService> logger)
{
_http = http;
_logger = logger;
}
public async Task<User?> GetUserAsync(int id, CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<User>($"api/users/{id}", ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch user {UserId}", id);
throw;
}
}
}
Authentication and Authorization
Blazor WebAssembly supports multiple authentication providers. For Azure AD:
// Program.cs
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://your-api/.default");
});
// Protecting a page
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
// Conditional UI
<AuthorizeView>
<Authorized>
<p>Welcome, @context.User.Identity?.Name</p>
</Authorized>
<NotAuthorized>
<p>Please log in</p>
</NotAuthorized>
</AuthorizeView>
Performance Optimization Checklist
- Enable Ahead-of-Time (AOT) compilation for faster runtime performance
- Use virtualization for large lists (
Virtualizecomponent) - Implement lazy loading for feature assemblies
- Avoid unnecessary re-renders by implementing
ShouldRender() - Use @key directive for dynamic lists to help the diffing algorithm
- Prefer async operations to keep the UI responsive
Key Takeaways
- Blazor WebAssembly runs entirely in the browser—no server connection required after initial load.
- Download size is the primary concern; use trimming, lazy loading, and compression.
- Choose state management based on complexity: Cascading for simple, Fluxor for complex apps.
- Typed HttpClient services provide clean, testable API integration.
- Blazor supports enterprise authentication with Azure AD, IdentityServer, and custom providers.
Conclusion
Blazor WebAssembly offers .NET developers a compelling path to full-stack development without JavaScript. While early 2020 previews had rough edges, the framework matured rapidly. Organizations with existing .NET expertise can leverage their skills to build modern, performant SPAs. The key to success is understanding the unique constraints of WebAssembly—particularly initial load time—and designing architectures that minimize their impact.
References
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.