Blazor in .NET 10 is a full-stack web UI framework: you write Razor components in C#, and each component can render statically on the server, interactively on the server over a SignalR connection, or in the browser on WebAssembly. Since .NET 8, one project type, the Blazor Web App, combines all of these render modes, so you choose interactivity per page or per component instead of per application. This guide explains the architecture and render modes, the component lifecycle, state management, forms, JavaScript interop, authentication and performance, and it covers what changed in .NET 10 and what is coming in .NET 11.
What Is Blazor?#
Blazor is ASP.NET Core's component-based UI framework. A component is a .razor file that combines markup and C# code and renders into a render tree. Blazor diffs each new render tree against the previous one and applies the minimal set of DOM changes. The same component model runs in several hosts:
- Blazor Web App (.NET 8 and later): server-rendered by default, with opt-in interactivity using the Server, WebAssembly or Auto render mode.
- Standalone Blazor WebAssembly: the whole app runs in the browser and can be served as static files.
- Blazor Hybrid: components render in a native WebView inside .NET MAUI, WPF or Windows Forms apps. See the Blazor Hybrid guide.
The older standalone Blazor Server template was replaced by the Blazor Web App template in .NET 8. Its behavior now corresponds to the Interactive Server render mode.
How Blazor Web Apps Work: Render Modes#
Every component in a Blazor Web App has a render mode that decides where it runs and whether it can respond to events:
| Render mode | Where code runs | Interactive | Connection | Best for |
|---|---|---|---|---|
| Static server (static SSR) | Server, per request | No (forms and enhanced navigation only) | None | Content pages, SEO, forms |
| Interactive Server | Server, in a circuit | Yes | SignalR, usually over WebSockets | Line-of-business apps with direct data access |
| Interactive WebAssembly | Browser | Yes | None after download | Rich client-side UIs and offline scenarios |
| Interactive Auto | Server first, then browser | Yes | SignalR until WebAssembly is cached | Public apps that need a fast first load |
Static SSR is the default. A statically rendered component runs once per HTTP request and writes HTML to the response, like a Razor Page. Enhanced navigation intercepts link clicks and form posts and patches the DOM instead of reloading the page, so even static pages feel like a single-page app.
Interactive Server keeps component instances in server memory inside a circuit, which is one per browser tab, and sends UI events and DOM diffs over SignalR. Interactive WebAssembly downloads the .NET runtime and your assemblies to the browser and runs them there. Interactive Auto uses Server for the first visit while the WebAssembly bundle downloads in the background, then uses WebAssembly on later visits. Auto never switches a component that is already on the page.
Interactive components are prerendered by default. The server first renders static HTML so users see content immediately, and then the component starts again in its interactive runtime. That second start is the root of several lifecycle subtleties covered later.
Getting Started with a Blazor Web App#
Create a project that supports every render mode with dotnet new blazor -n Portal --interactivity Auto. The --interactivity option accepts None, Server, WebAssembly or Auto, and --all-interactive applies the mode globally instead of per page. Choosing WebAssembly or Auto creates two projects: the server project and a .Client project for components that must run in the browser. The server's Program.cs, simplified from the .NET 10 template, looks like this:
using Portal.Components;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents() // Enables Interactive Server
.AddInteractiveWebAssemblyComponents(); // Enables Interactive WebAssembly and Auto
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets(); // Fingerprinted, compressed static files
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Portal.Client._Imports).Assembly);
app.Run();The services calls enable the runtimes, and the endpoint calls expose them. A component still renders statically until you give it an interactive render mode.
Streaming Rendering for Static SSR#
Static SSR normally waits for every asynchronous operation before sending HTML. Streaming rendering sends the page immediately with placeholder content, keeps the response open, and patches in the final markup when the data arrives:
@page "/orders"
@attribute [StreamRendering]
@inject IOrderService Orders
@if (orders is null)
{
<p>Loading recent orders...</p>
}
else
{
<OrderTable Orders="orders" />
}
@code {
private IReadOnlyList<OrderSummary>? orders;
protected override async Task OnInitializedAsync() =>
orders = await Orders.GetRecentAsync();
}Streaming requires that no proxy or middleware buffers the response. If a host does buffer it, the page still works but loads all at once. In .NET 8 the attribute takes an argument, [StreamRendering(true)].
The Blazor Component Lifecycle#
ComponentBase calls a fixed sequence of methods: SetParametersAsync, then OnInitialized{Async} once, then OnParametersSet{Async} every time parameters change, then a render, then OnAfterRender{Async}(firstRender). Disposal happens through IDisposable or IAsyncDisposable. Three rules prevent most bugs:
- Initialization runs twice with prerendering.
OnInitializedAsyncexecutes once during prerendering and again when the interactive runtime starts, which doubles database calls unless you persist state. OnAfterRender{Async}never runs during prerendering or static SSR, because no live DOM exists yet. That is the only safe place for first-time JavaScript interop.- External events need
InvokeAsync. Timers and message handlers that aren't Blazor events must callInvokeAsync(StateHasChanged)to rerender on the component's synchronization context.
@implements IAsyncDisposable
@inject IJSRuntime JS
@inject IPriceFeed Feed
<p>@Symbol: @price.ToString("C")</p>
@code {
[Parameter, EditorRequired] public string Symbol { get; set; } = "";
private readonly CancellationTokenSource cts = new();
private IJSObjectReference? module;
private decimal price;
// Runs on first render and whenever the parent passes a new Symbol.
protected override async Task OnParametersSetAsync() =>
price = await Feed.GetPriceAsync(Symbol, cts.Token);
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) // Never during prerendering: a DOM exists now
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./Components/StockTicker.razor.js");
await module.InvokeVoidAsync("highlight", Symbol);
}
}
public async ValueTask DisposeAsync()
{
cts.Cancel();
if (module is not null)
{
try { await module.DisposeAsync(); }
catch (JSDisconnectedException) { } // The circuit is already gone
}
cts.Dispose();
}
}State Management and Persistent Component State#
State lives in different places depending on the render mode. Component fields are per instance. Scoped services are per circuit in Interactive Server, and effectively per browser tab in WebAssembly, where scoped behaves like singleton. Cascading values share state down a subtree. Browser storage survives reloads, and ProtectedLocalStorage and ProtectedSessionStorage encrypt it for server-side components.
The prerendering handoff needs special care. .NET 10 adds a declarative [PersistentState] attribute. During prerendering the framework serializes the property into the page, and the interactive instance restores it instead of querying again:
@page "/movies"
@inject IMovieService MovieService
<MovieGrid Movies="Movies" />
@code {
[PersistentState] // .NET 10: survives the prerender-to-interactive handoff
public List<Movie>? Movies { get; set; }
protected override async Task OnInitializedAsync() =>
Movies ??= await MovieService.GetMoviesAsync();
}The property must be public. AllowUpdates = true lets enhanced navigation refresh read-mostly data. RestoreBehavior can skip the prerendered value or the reconnection snapshot, and RegisterPersistentService applies the same model to scoped services.
.NET 10 also adds circuit state persistence for Interactive Server. When a connection drops for a long time, or when a circuit is paused with Blazor.pauseCircuit(), component and scoped-service state is saved, by default in memory for up to 1,000 circuits for two hours, or in HybridCache for distributed storage. It is restored on resume without losing unsaved work. A full page refresh still discards it.
Forms and Validation in Blazor#
EditForm binds a model to input components, runs validators and raises OnValidSubmit. In static SSR, a form needs a unique FormName, and the model property needs [SupplyParameterFromForm] so the posted values bind back to it. EditForm adds antiforgery protection automatically when the app calls UseAntiforgery.
.NET 10 upgrades DataAnnotationsValidator with source-generated validation from Microsoft.Extensions.Validation. That adds nested objects, collections and IValidatableObject support. Opt in by calling builder.Services.AddValidation(), declaring the model in a .cs file (not a .razor file) and marking the root type with [ValidatableType]:
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Validation;
#pragma warning disable ASP0029 // [ValidatableType] is experimental in .NET 10
[ValidatableType]
public sealed class ShippingForm
{
[Required, StringLength(80)]
public string? FullName { get; set; }
public Address Address { get; set; } = new(); // Validated recursively
}
#pragma warning restore ASP0029
public sealed class Address
{
[Required]
public string? Street { get; set; }
[Required, RegularExpression(@"^\d{5}$", ErrorMessage = "Use a 5-digit ZIP code.")]
public string? PostalCode { get; set; }
}<EditForm Model="Model" FormName="shipping" OnValidSubmit="SaveAsync" Enhance>
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="Model!.FullName" />
<InputText @bind-Value="Model!.Address.Street" />
<ValidationMessage For="() => Model!.Address.Street" />
<InputText @bind-Value="Model!.Address.PostalCode" />
<button type="submit">Save</button>
</EditForm>
@code {
[SupplyParameterFromForm]
private ShippingForm? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
private Task SaveAsync() => Shipping.SaveAsync(Model!);
}.NET 10 also adds an InputHidden component. The .NET 11 previews go further with client-side validation for static SSR forms, which uses the same .NET attributes and needs no server round trip, and with asynchronous validation through EditContext.ValidateAsync.
JavaScript Interop#
Blazor calls JavaScript through IJSRuntime and receives calls through [JSInvokable] methods with a DotNetObjectReference. Prefer collocated JavaScript modules, such as StockTicker.razor.js, loaded with import as shown in the lifecycle example. Modules are scoped, cacheable and disposable. .NET 10 adds APIs that remove many one-line wrapper functions:
// .NET 10: construct JS objects and access properties directly.
await using var map = await JS.InvokeConstructorAsync("mapLib.Map", mapElement, "streets");
await map.InvokeVoidAsync("setZoom", 12);
var zoom = await map.GetValueAsync<int>("zoom");
await JS.SetValueAsync("appSettings.theme", "dark");In Interactive Server, every interop call is a network round trip. Batch the work, avoid calls inside tight loops, and expect JSDisconnectedException during disposal. In WebAssembly, IJSInProcessRuntime offers synchronous calls, and the [JSImport] and [JSExport] attributes provide lower-overhead interop for hot paths.
Blazor Performance: Virtualization, AOT and Rendering#
Most Blazor performance work comes down to rendering less, downloading less and calling out less:
- Virtualize long lists.
Virtualizerenders only visible rows plus an overscan buffer, and anItemsProviderfetches rows on demand. - Control rerendering. Override
ShouldRender, pass immutable parameters, and use@keyin loops so the diff can reuse elements. - Use AOT when CPU-bound. Without it, WebAssembly runs your IL on an interpreter with partial JIT support (the Jiterpreter). Setting
<RunAOTCompilation>true</RunAOTCompilation>and installing thewasm-toolsworkload compiles to WebAssembly at publish time. Microsoft's documentation says most AOT builds are roughly twice the size of IL builds, so reserve AOT for compute-heavy apps. - Trim and lazy-load assemblies to cut the WebAssembly download.
<div style="height: 600px; overflow-y: auto">
<Virtualize ItemsProvider="LoadOrdersAsync" ItemSize="48" Context="order">
<ItemContent>
<OrderRow Order="order" />
</ItemContent>
<Placeholder>
<div class="order-row">Loading...</div>
</Placeholder>
</Virtualize>
</div>
@code {
private async ValueTask<ItemsProviderResult<OrderSummary>> LoadOrdersAsync(
ItemsProviderRequest request)
{
var page = await Orders.GetPageAsync(
request.StartIndex, request.Count, request.CancellationToken);
return new ItemsProviderResult<OrderSummary>(page.Items, page.TotalCount);
}
}For Interactive Server, remember that each circuit holds memory on the server, so keep per-circuit state small and load test with realistic numbers of concurrent users.
What's New in Blazor in .NET 10#
.NET 10, the current LTS release, focused on reliability and the gaps between render modes:
- Declarative persistent state with
[PersistentState], plus support for enhanced navigation and custom serializers. - Circuit state persistence, with
Blazor.pauseCircuit()andBlazor.resumeCircuit()for Interactive Server. - Not Found handling through
NavigationManager.NotFound()and aNotFoundPageparameter onRouter. The template ships aNotFound.razorpage. - A
ReconnectModalcomponent in the template that works with strict Content Security Policies, andNavigateTothat no longer throws during static SSR whenBlazorDisableThrowNavigationExceptionis set, which the template does. - Better validation for nested objects and collections, and the
InputHiddencomponent. - Performance and diagnostics: the Blazor script served as a fingerprinted static web asset, preloaded framework assets, response streaming by default for
HttpClientin WebAssembly, and new metrics and tracing. - JavaScript interop constructors and property access, plus passkeys in ASP.NET Core Identity.
Blazor vs Razor Pages vs JavaScript SPAs#
| Criterion | Blazor Web App | Razor Pages or MVC | JavaScript SPA with an API |
|---|---|---|---|
| Language | C# end to end | C# on the server, JavaScript for interactivity | TypeScript or JavaScript on the client |
| Interactivity | Per component, in four render modes | Page-level; interactivity is hand-written | Full client-side |
| Code sharing with the backend | Models, validation and services | Models and validation | Contract only, through OpenAPI |
| Initial load | Fast with static SSR and prerendering | Fast | Depends on bundle size and SSR framework |
| Ecosystem | Growing component libraries | Mature | Largest |
| Best fit | .NET teams building interactive apps | Content sites and simple forms | Frontend-specialized teams |
If your pages are mostly content, see Razor Pages vs MVC. If you want native desktop or mobile apps, Blazor Hybrid reuses the same components.
Best Practices#
- Default to static SSR and add interactivity only where users need it. Islands of interactivity keep pages fast and servers cheap.
- Persist prerendered state with
[PersistentState]so data loads once, not twice. - Put data access behind interfaces when using Auto. The same component must work on the server with direct database access and in the browser through HTTP APIs.
- Do JavaScript interop in
OnAfterRenderAsync, dispose module references, and catchJSDisconnectedException. - Keep circuits lean in Interactive Server by avoiding large per-user caches in scoped services.
- Enforce authorization on the server, whatever the UI hides.
- Measure before using AOT. It helps CPU-heavy code but roughly doubles the download.
Common Pitfalls#
- Expecting interactivity from a static component. Without a render mode,
@onclickdoes nothing. Check the page's render mode first. - Double data loading caused by prerendering, when state isn't persisted.
- Calling JavaScript in
OnInitializedAsync, which fails during prerendering. - Missing
FormNameor[SupplyParameterFromForm]on static SSR forms, so posts don't bind. - Putting WebAssembly or Auto components in the server project, where they never reach the browser bundle.
- Updating UI from background threads without
InvokeAsync, which throws or silently skips renders.
Frequently Asked Questions#
Which Blazor render mode should I choose?#
Start with static SSR for content and forms, then add Interactive Server for internal apps with low latency to the server, or Interactive WebAssembly when you need client-side execution, offline support or less server load. Choose Auto for public apps that want a fast first visit and client-side execution afterward, and accept the cost of supporting both environments.
Is Blazor Server still supported?#
Yes. The standalone Blazor Server project template was folded into the Blazor Web App in .NET 8, and its model lives on as the Interactive Server render mode. .NET 10 strengthens it with circuit state persistence and pause and resume support.
Why does my component load data twice?#
Interactive components are prerendered by default, so OnInitializedAsync runs once on the server for the static HTML and again when the component becomes interactive. Use [PersistentState] in .NET 10 to carry the data across, or disable prerendering for that component.
Can Blazor WebAssembly call my database directly?#
No. WebAssembly code runs in the user's browser, so it must call secure web APIs that you host, such as Minimal API endpoints. Anything in the client bundle, including connection strings, can be read by users.
What is coming for Blazor in .NET 11?#
.NET 11 reached Release Candidate 1 on September 8, 2026, and is due in November 2026. Its previews added client-side validation for static SSR forms, async form validation, CacheView for caching SSR output, automatic circuit pausing for inactive tabs, variable-height virtualization and experimental Blazor AI components for agentic user interfaces.
Summary#
- A Blazor Web App mixes static SSR, streaming rendering and three interactive render modes, chosen per page or per component.
- Prerendering improves first paint but runs initialization twice.
[PersistentState]in .NET 10 fixes the double work. - The lifecycle rules matter: JavaScript interop belongs in
OnAfterRenderAsync, external updates go throughInvokeAsync, and resources get disposed. - .NET 10 improves validation, Not Found handling, reconnection, circuit persistence and diagnostics, and .NET 11 extends forms, caching and virtualization.
- Authorize on the server, virtualize large lists, and reserve WebAssembly AOT for CPU-heavy work.