Blazor in 2026: Server vs WebAssembly vs United — A Practical Guide
Blazor in .NET 8/9 has matured into a production-ready framework, but the multiple render modes can be confusing. Let's break down what each mode does, when to use it, and real-world performance trade-offs so you can make informed decisions for your next project.
How Each Render Mode Works
- Blazor Server — components run on the server; every UI interaction round-trips over SignalR
- Blazor WebAssembly — .NET runtime downloads to the browser; everything runs client-side
- Blazor United (Interactive Auto) — mix render modes per component: Static SSR, Interactive Server, Interactive WASM, or Auto (server first, then switches to WASM)
@page "/dashboard"
@rendermode InteractiveServer // Server-side via SignalR
@page "/editor"
@rendermode InteractiveWebAssembly // Runs in browser
@page "/analytics"
@rendermode InteractiveAuto // Server first, then WASM
Real-World Performance Data
| Metric | Server | WASM | Auto (United) |
|-------------------------|------------|-------------|---------------|
| Time to Interactive | 120ms | 3.2s | 130ms (server) |
| Initial download size | ~50 KB | ~8.5 MB | ~55 KB + lazy |
| Input latency (typing) | 30-80ms | <5ms | <5ms (wasm) |
| Server memory per user | ~15-25 MB | None | ~5-10 MB |
| Max concurrent users* | ~500 | Unlimited | ~2000 |
The input latency catches most teams off guard — on a 150ms mobile connection, Blazor Server forms feel sluggish. This metric alone should drive your decision for form-heavy apps.
When to Use Each Mode
Use Server for internal apps on corporate networks with predictable user counts and sensitive business logic that shouldn't leave the server. The always-connected SignalR model keeps all code and data on the server, which simplifies security but introduces a dependency on stable network connections.
Use WebAssembly for public-facing, input-heavy apps that need offline support or have unpredictable user counts. Once the runtime is downloaded, all processing happens in the browser — zero server load per user.
Use United when different parts of your app have different requirements — most pages are static SSR, with interactive islands only where needed. This is the recommended default for new projects in .NET 8+.
Setting Up a United Project
When creating a new Blazor Web App, the template configures both server and client projects. Here's how the Program.cs wires up both render modes:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddScoped<IProductService, ServerProductService>();
var app = builder.Build();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(ClientApp._Imports).Assembly);
app.Run();
This setup lets you choose the render mode per-page or per-component. Static SSR pages have zero JavaScript overhead, while interactive components load only what they need. The key insight is that most pages in a typical application don't need interactivity — product listings, documentation, and content pages work perfectly as static SSR.
United Pattern: Service Abstraction
For components that may run in both Server and WASM contexts, abstract data access behind interfaces:
public interface IProductService
{
Task<List<Product>> GetProductsAsync(string? category = null);
Task<Product?> GetByIdAsync(int id);
}
// Server: direct DB access
public class ServerProductService(AppDbContext db) : IProductService
{
public async Task<List<Product>> GetProductsAsync(string? category)
{
var query = db.Products.AsNoTracking();
if (category is not null)
query = query.Where(p => p.Category == category);
return await query.ToListAsync();
}
public async Task<Product?> GetByIdAsync(int id) => await db.Products.FindAsync(id);
}
// Client: calls HTTP API
public class ClientProductService(HttpClient http) : IProductService
{
public async Task<List<Product>> GetProductsAsync(string? category)
=> await http.GetFromJsonAsync<List<Product>>(
category is not null ? $"/api/products?category={category}" : "/api/products") ?? [];
public async Task<Product?> GetByIdAsync(int id)
=> await http.GetFromJsonAsync<Product>($"/api/products/{id}");
}
Register the correct implementation in each project — ServerProductService in the server project and ClientProductService in the WASM client project. This pattern keeps your Razor components completely agnostic about where they're running and makes unit testing straightforward since you can mock the interface.
Handling Pre-rendering with PersistentComponentState
Pre-rendering is enabled by default and runs OnInitializedAsync twice — once on the server for the initial HTML, then again when the component becomes interactive. Use PersistentComponentState to avoid duplicate API calls:
@inject PersistentComponentState ApplicationState
@inject IProductService ProductService
@code {
private List<Product> _products = [];
private PersistingComponentStateSubscription _persistingSubscription;
protected override async Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
if (!ApplicationState.TryTakeFromJson<List<Product>>("products", out var restored))
{
_products = await ProductService.GetProductsAsync();
}
else
{
_products = restored!;
}
}
private Task PersistData()
{
ApplicationState.PersistAsJson("products", _products);
return Task.CompletedTask;
}
public void Dispose() => _persistingSubscription.Dispose();
}
Common Pitfalls
- Don't default to InteractiveAuto everywhere — you'll end up with a 12MB WASM download. Only use Auto where client-side execution genuinely helps.
- Render mode is inherited. Keep interactive islands as small as possible in your component tree.
- Pre-rendering runs
OnInitializedAsynctwice. UsePersistentComponentStateto avoid duplicate API calls, as shown above. - Test on real networks. Blazor Server feels instant on localhost but sluggish on 4G mobile connections.
- Don't put
@rendermodeon layout components — this forces every page into that render mode and defeats the purpose of United.
Key Takeaways
- Start with static SSR by default. Only add interactivity where users actually need it.
- Use Interactive Server for internal apps with low-latency networks.
- Use Interactive WebAssembly for input-heavy public apps where latency matters.
- Use Interactive Auto sparingly — reserve it for components needing both fast initial load and client-side responsiveness.
- Invest in the service abstraction layer early — it makes your codebase more testable and keeps migration options open.
References
- ASP.NET Core Blazor render modes — Official documentation on Interactive Server, WebAssembly, and Auto render modes
- Blazor overview — Getting started with Blazor in .NET 8 and 9
- ASP.NET Core Blazor prerendering — How to handle prerendering and PersistentComponentState
- Blazor samples on GitHub — Official Blazor sample applications from the .NET team
Share this post
Comments
Ajit Gangurde
Software Engineer II at Microsoft | 15+ years in .NET & Azure