Blazor in 2026: Server vs WebAssembly vs United — A Practical Guide
Blazor has come a long way since its experimental days, and with .NET 8 and 9, it's matured into a framework I'm comfortable recommending for production business applications. But the introduction of multiple render modes has also made it more confusing. I talk to .NET developers every week who aren't sure whether they should use Server, WebAssembly, or the new "United" approach — and I don't blame them. The naming alone is a challenge.
I've shipped applications using all three render modes, including a migration from Blazor Server to the United model that took our team about six weeks. In this post, I'll break down what each mode actually does, when to use it, what the real-world performance looks like, and how to think about migration. No theoretical hand-waving — just patterns that have worked in practice.
Let me be clear about terminology: what Microsoft calls "Blazor Web App" with interactive render modes is what the community often calls "Blazor United." It's the model where you can mix Server, WebAssembly, and static SSR within a single application. I'll use "United" throughout this post because it's the term most developers recognize.
How Each Render Mode Works
Understanding the mechanics helps you make informed trade-offs.
Blazor Server runs your components on the server. Every UI interaction (button click, text input, dropdown selection) sends a message over a SignalR WebSocket connection to the server, which executes the component logic, computes the DOM diff, and sends the diff back to the browser. The browser applies the diff. Your C# code never runs in the browser.
Blazor WebAssembly (WASM) downloads the .NET runtime and your application DLLs to the browser. Everything runs client-side. After the initial download, there's no server dependency for UI rendering — though you still call APIs for data.
Blazor United (Interactive Auto) lets you choose the render mode per component or per page. A component can be:
- Static SSR — rendered on the server as HTML, no interactivity
- Interactive Server — Blazor Server mode via SignalR
- Interactive WebAssembly — runs in the browser via WASM
- Interactive Auto — starts with Server for instant interactivity, then switches to WASM once the runtime is downloaded
Here's how you specify render modes in .NET 9:
// App.razor — setting the default render mode
<Routes @rendermode="InteractiveServer" />
// Or per-component in a page
@page "/dashboard"
@rendermode InteractiveServer
<PageTitle>Dashboard</PageTitle>
<DashboardGrid />
// A component that should run client-side
@page "/editor"
@rendermode InteractiveWebAssembly
<RichTextEditor @bind-Content="content" />
// Auto mode — server first, then switches to WASM
@page "/analytics"
@rendermode InteractiveAuto
<AnalyticsCharts Data="chartData" />
Real-World Performance Data
I've benchmarked all three modes on the same application — an internal business dashboard with data grids, charts, and form-heavy CRUD operations. Here are the numbers from production:
| Metric | Server | WASM | Auto (United) |
|-------------------------|------------|-------------|---------------|
| Time to First Byte | 45ms | 52ms | 48ms |
| Time to Interactive | 120ms | 3.2s | 130ms (server) |
| Initial download size | ~50 KB | ~8.5 MB | ~55 KB + lazy |
| Subsequent page loads | Instant* | Instant | Instant |
| Input latency (typing) | 30-80ms | <5ms | <5ms (wasm) |
| Works offline | No | Yes** | Partial |
| Server memory per user | ~15-25 MB | None | ~5-10 MB |
| Max concurrent users*** | ~500 | Unlimited | ~2000 |
* Via SignalR, feels instant but depends on connection quality ** With service worker and cached data *** Per server instance, approximate, depends on component complexity
The input latency difference is the one that catches most teams off guard. On Blazor Server, every keystroke round-trips to the server. On a local network, you won't notice it. On a 150ms latency mobile connection, typing in a form feels sluggish. This single metric should drive your decision for form-heavy applications.
When to Use Each Mode
After shipping multiple Blazor applications, here's my decision framework:
Use Blazor Server When:
- Your users are on a corporate network with low latency
- You need to protect sensitive business logic (it never leaves the server)
- Your application is internal-facing with a predictable user count
- You want the fastest development speed (no WASM serialization concerns)
// Server mode is great for admin dashboards with sensitive data
@page "/admin/users"
@rendermode InteractiveServer
@attribute [Authorize(Roles = "Admin")]
<h2>User Management</h2>
@* All data access happens server-side — no API layer needed *@
<QuickGrid Items="@_users" Pagination="@_pagination">
<PropertyColumn Property="@(u => u.Email)" Sortable="true" />
<PropertyColumn Property="@(u => u.Role)" Sortable="true" />
<PropertyColumn Property="@(u => u.LastLogin)"
Format="yyyy-MM-dd HH:mm" Sortable="true" />
<TemplateColumn Title="Actions">
<button @onclick="() => EditUser(context)">Edit</button>
<button @onclick="() => DisableUser(context)"
class="danger">Disable</button>
</TemplateColumn>
</QuickGrid>
@code {
private IQueryable<AppUser>? _users;
private PaginationState _pagination = new() { ItemsPerPage = 20 };
// Direct database access — no API serialization overhead
[Inject] private AppDbContext Db { get; set; } = default!;
protected override void OnInitialized()
{
_users = Db.Users.AsNoTracking().OrderBy(u => u.Email);
}
}
Use Blazor WebAssembly When:
- Your application needs to work offline or with poor connectivity
- You have many concurrent users and want to minimize server load
- Input-heavy UIs where latency matters (forms, editors, drawing tools)
- Public-facing applications where you can't predict user counts
Use Blazor United (Auto) When:
- You want the best of both worlds — fast initial load + responsive client-side interaction
- Different parts of your app have different requirements
- You're building a content-heavy site where most pages are informational (static SSR) with a few interactive sections
Building a United Application: Practical Patterns
The United model requires thinking about which components need interactivity and which don't. Here's a pattern I use for content-heavy applications:
// Layout that mixes static and interactive content
@inherits LayoutComponentBase
<div class="page">
@* Navigation is static — no interactivity needed *@
<NavMenu />
<main class="content">
@* Static breadcrumb — rendered as plain HTML *@
<Breadcrumb />
@Body
@* Footer is static *@
<Footer />
</main>
@* Chat widget is interactive — runs on the server *@
<SupportChat @rendermode="InteractiveServer" />
</div>
The key insight: most of your UI doesn't need interactivity. Navigation, headers, footers, content display — these can all be static SSR, which means zero JavaScript, zero SignalR connections, and instant rendering.
For components that need to work in both Server and WASM contexts, you need to abstract your data access behind interfaces:
// Shared interface — works in both render modes
public interface IProductService
{
Task<List<Product>> GetProductsAsync(string? category = null);
Task<Product?> GetByIdAsync(int id);
Task SaveAsync(Product product);
}
// Server-side implementation — direct database access
public class ServerProductService : IProductService
{
private readonly AppDbContext _db;
public ServerProductService(AppDbContext db) => _db = db;
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);
public async Task SaveAsync(Product product)
{
_db.Products.Update(product);
await _db.SaveChangesAsync();
}
}
// Client-side implementation — calls HTTP API
public class ClientProductService : IProductService
{
private readonly HttpClient _http;
public ClientProductService(HttpClient http) => _http = http;
public async Task<List<Product>> GetProductsAsync(string? category)
{
var url = category is not null
? $"/api/products?category={category}"
: "/api/products";
return await _http.GetFromJsonAsync<List<Product>>(url) ?? [];
}
public async Task<Product?> GetByIdAsync(int id)
=> await _http.GetFromJsonAsync<Product>($"/api/products/{id}");
public async Task SaveAsync(Product product)
=> await _http.PutAsJsonAsync($"/api/products/{product.Id}", product);
}
Register the correct implementation in each project:
// Server project — Program.cs
builder.Services.AddScoped<IProductService, ServerProductService>();
// Client project — Program.cs
builder.Services.AddScoped<IProductService, ClientProductService>();
builder.Services.AddHttpClient<ClientProductService>(client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});
Migration Tips: Server to United
If you're migrating an existing Blazor Server app to the United model, here's the approach that worked for us:
Phase 1: Inventory your components. Categorize each page and component as "needs interactivity" or "static is fine." In our application, 60% of pages were read-only displays that could be static SSR.
Phase 2: Extract data access behind interfaces. This is the biggest refactoring effort. Any component that directly accesses DbContext or server-side services needs an abstraction layer if it might run on WASM.
Phase 3: Migrate page by page. Start with the simplest static pages, then move to Interactive Server for complex pages, and only use Interactive Auto where you specifically need client-side performance.
// Before: Blazor Server component with direct DB access
@page "/products"
@inject AppDbContext Db
<h2>Products</h2>
@foreach (var product in _products)
{
<ProductCard Product="product" />
}
@code {
private List<Product> _products = [];
protected override async Task OnInitializedAsync()
{
_products = await Db.Products.ToListAsync();
}
}
// After: United-compatible component with service abstraction
@page "/products"
@inject IProductService ProductService
@rendermode InteractiveServer // Can switch to Auto later
<h2>Products</h2>
@foreach (var product in _products)
{
<ProductCard Product="product" />
}
@code {
private List<Product> _products = [];
protected override async Task OnInitializedAsync()
{
_products = await ProductService.GetProductsAsync();
}
}
Phase 4: Add streaming SSR for slow-loading content. This is a .NET 8+ feature that's particularly powerful in the United model:
@page "/reports"
@attribute [StreamRendering]
<h2>Monthly Report</h2>
@if (_reportData is null)
{
<LoadingSpinner />
}
else
{
<ReportTable Data="_reportData" />
}
@code {
private ReportData? _reportData;
protected override async Task OnInitializedAsync()
{
// Page renders immediately with the spinner,
// then streams the update when data is ready
_reportData = await ReportService.GenerateAsync();
}
}
Common Pitfalls to Avoid
Don't default to InteractiveAuto everywhere. I've seen teams set Auto as the global render mode and wonder why their WASM download is 12MB. Only use Auto for components that genuinely benefit from client-side execution.
Watch your component tree. Render mode is inherited. If a parent is Interactive Server, all children are too — even if they don't need interactivity. Structure your component tree so that interactive islands are as small as possible.
Test on real networks. Blazor Server works beautifully on localhost with <1ms latency. Test on a 4G mobile connection with 150ms latency. You'll quickly learn which components need to run client-side.
Pre-render carefully. Pre-rendering means your component's OnInitializedAsync runs twice — once on the server for the initial HTML, and once when the interactive runtime connects. Use PersistentComponentState to avoid duplicate API calls:
@inject PersistentComponentState ApplicationState
@code {
private List<Product> _products = [];
private PersistingComponentStateSubscription _persistingSubscription;
protected override async Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(() =>
{
ApplicationState.PersistAsJson("products", _products);
return Task.CompletedTask;
});
if (!ApplicationState.TryTakeFromJson<List<Product>>(
"products", out var restored))
{
_products = await ProductService.GetProductsAsync();
}
else
{
_products = restored!;
}
}
}
Conclusion
Blazor in 2026 is the most capable it's ever been, but with capability comes complexity. Here's my practical advice:
- Start with static SSR by default. Only add interactivity where users actually need it. This gives you the best performance baseline.
- Use Interactive Server for internal apps where you control the network. The development experience is unmatched — direct database access, no API layer, no serialization.
- Use Interactive WebAssembly for input-heavy public apps where latency matters and you can't predict user counts.
- Use Interactive Auto sparingly — it's powerful but adds complexity. Reserve it for components where fast initial load AND client-side responsiveness both matter.
- Invest in the service abstraction layer early. Whether or not you migrate to United today, having clean data access interfaces makes your codebase more testable and flexible.
The render mode decision isn't permanent. The United model lets you change your mind per-component without rewriting your application. Start conservative, measure real user experience, and adjust.
Comments
Ajit Gangurde
Software Engineer II at Microsoft | 15+ years in .NET & Azure
Related Posts
Mar 14, 2026
Feb 28, 2026