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.
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.
Use WebAssembly for public-facing, input-heavy apps that need offline support or have unpredictable user counts.
Use United when different parts of your app have different requirements — most pages are static SSR, with interactive islands only where needed.
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}");
}
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. - Test on real networks. Blazor Server feels instant on localhost but sluggish on 4G mobile connections.
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.
Comments
Ajit Gangurde
Software Engineer II at Microsoft | 15+ years in .NET & Azure
Related Posts
Mar 14, 2026
Feb 28, 2026