Discover everything new in Blazor .NET 10: persistent state management, improved JavaScript interop, passkey authentication, and major performance gains. A comprehensive guide with code examples for .NET developers.
Table of Contents
- Why Your Blazor Code is About to Get Simpler
- The Boilerplate Problem
- .NET 10: The "We Actually Fixed State" Release
- Performance: Blazor .NET 10 Benchmarks
- JavaScript Interop in Blazor .NET 10
- Hot Reload for WebAssembly
- Form Validation Source Generator
- 404 Handling That Makes Sense
- Navigation Improvements
- Reconnection UI Updates
- Passkey Authentication Support
- QuickGrid Updates
- WebAssembly Improvements
- Migration Checklist
- Common Mistakes to Avoid
- When to Upgrade
- Frequently Asked Questions
- Final Thoughts
Why Your Blazor Code is About to Get Simpler
Blazor in .NET 10 represents a significant leap forward for Microsoft's web UI framework. I remember when Blazor was the weird kid at the .NET party. "You want to run C# in the browser? That's adorable." Fast forward to 2025, and Blazor .NET 10 is no longer the underdog—it's a genuine contender.
Blazor has always worked. But some patterns required more ceremony than they should.
.NET 10 finally streamlines the rough edges. For the official overview, see Microsoft's What's new in ASP.NET Core 10.0 documentation.
The Boilerplate Problem
Here's what many developers experienced. You start a Blazor project. You're excited. You write components. They work. Life is good.
Then your boss says: "Why does the page flash white during navigation?"
And you discover prerendering. And hydration. And the double-render problem. The framework handled all of this—but you had to write the wiring yourself. Serializing state manually, juggling OnInitializedAsync vs OnAfterRenderAsync, building abstractions that felt like they should've been built-in.
Six months later, your Program.cs:
builder.Services.AddScoped<IStateContainer, StateContainer>();
builder.Services.AddScoped<IPreRenderStateService, PreRenderStateService>();
builder.Services.AddScoped<IHydrationHelper, HydrationHelper>();
builder.Services.AddScoped<IComponentStateManager, ComponentStateManager>();
builder.Services.AddScoped<ICircuitHandler, CustomCircuitHandler>();
builder.Services.AddSingleton<IReconnectionStateTracker, ReconnectionStateTracker>();
// Services that worked great, but shouldn't have been your job to write
The component that fetches weather data? It fetches it twice. Once during prerender, once during interactive mode. Your API team hates you.
.NET 10: The "We Actually Fixed State" Release
Let me show you what's new. And why it matters.
Persistent State That Doesn't Make You Cry
Before .NET 10:
@inject PersistentComponentState ApplicationState
@code {
private WeatherForecast[]? forecasts;
private PersistingComponentStateSubscription _subscription;
protected override async Task OnInitializedAsync()
{
_subscription = ApplicationState.RegisterOnPersisting(PersistData);
if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>("forecasts", out forecasts))
{
forecasts = await FetchForecasts();
}
}
private Task PersistData()
{
ApplicationState.PersistAsJson("forecasts", forecasts);
return Task.CompletedTask;
}
public void Dispose()
{
_subscription.Dispose();
}
}
That's 25 lines of ceremony just to not double-fetch data. Every. Single. Component.
After .NET 10:
@code {
[PersistentState]
public WeatherForecast[]? Forecasts { get; set; }
protected override async Task OnInitializedAsync()
{
Forecasts ??= await FetchForecasts();
}
}
One attribute. That's it. The runtime handles serialization, hydration, and cleanup. Your API team might even invite you to lunch again. Learn more about Blazor state management in the official docs.
Circuit State Persistence (The "My WebSocket Died" Problem)
You know the scenario. User fills out a complex form. Their internet hiccups. WebSocket dies. Blazor Server reconnects.
All their data? Gone. They start over. They leave your app. They write a bad review.
.NET 10 fixes this:
// When the user goes idle or connection is unstable
Blazor.pause();
// Later, when they're back
await Blazor.resume();
When you call Blazor.pause() before the connection drops, .NET 10 persists the circuit state to browser storage. You can resume it later—even if the original circuit was evicted on the server. In practice, how long this works depends on your server settings and the browser's storage lifecycle. Think "much more resilient" rather than "infinite session immortality."
This makes Blazor Server significantly more resilient for demanding enterprise scenarios. For more details, see Microsoft's Blazor Server reconnection documentation.
Performance: Blazor .NET 10 Benchmarks
Here's the before and after for a typical Blazor Web App (your exact numbers will vary based on configuration and app shape):
| Asset | .NET 9 | .NET 10 | Improvement |
|---|---|---|---|
blazor.web.js |
~183 KB | ~43 KB | ~76% smaller |
| Boot manifest | Separate file | Inlined in dotnet.js
|
1 fewer request |
| Asset delivery | Embedded resources | Static with fingerprinting | Better caching |
That JavaScript bundle? It went on a serious diet. Your lighthouse scores will thank you.
But wait, there's more. .NET 10 preloads Blazor framework assets automatically via Link headers in Blazor Web Apps and high-priority downloads in standalone WebAssembly. The runtime downloads while your page is rendering instead of afterwards.
First paint happens. User sees content. Meanwhile, Blazor runtime downloads in the background. No special component required—the framework handles it.
It's the difference between "this app feels slow" and "this app feels instant."
JavaScript Interop in Blazor .NET 10
Old JS interop was... functional. You could call functions. You could pass primitives. You could pray your JSON serialization didn't explode.
.NET 10 adds actual object semantics:
// Create a JS object instance
var chart = await JS.InvokeConstructorAsync<IJSObjectReference>(
"Chart",
canvasElement,
chartConfig
);
// Read properties
var currentValue = await chart.GetValueAsync<int>("currentValue");
// Set properties
await chart.SetValueAsync("animationDuration", 500);
// For performance-critical code (in-process only)
var syncValue = chart.GetValue<int>("dataLength");
You're working with actual objects now. Not just strings and prayers. For deeply nested property paths, you'll typically still write a small JS helper and call that from .NET. See Call JavaScript from .NET for the complete API reference.
The Real Win: Constructor Support
Before, creating a JS object looked like this:
await JS.InvokeVoidAsync("eval", "window.myChart = new Chart(canvas, config)");
var reference = await JS.InvokeAsync<IJSObjectReference>("eval", "window.myChart");
Eval. In production code. I've seen it. I've written it. We all have.
Now:
var myChart = await JS.InvokeConstructorAsync<IJSObjectReference>("Chart", canvas, config);
No eval. No global pollution. Just clean interop.
Hot Reload for WebAssembly
Hot Reload in Blazor has always been a bit of a mixed bag. Server-side? Pretty good. WebAssembly? Required manual setup and sometimes just... didn't.
.NET 10 migrated to a general-purpose Hot Reload implementation for WebAssembly. The SDK now handles it automatically.
<!-- This is now true by default for Debug builds -->
<PropertyGroup>
<WasmEnableHotReload>true</WasmEnableHotReload>
</PropertyGroup>
You don't need to add this. It's already on. Edit a .razor file, save, and see the changes instantly. No rebuild. No browser refresh. No configuration.
For teams with custom build configurations:
<!-- Enable for non-Debug configurations -->
<PropertyGroup Condition="'$(Configuration)' == 'Staging'">
<WasmEnableHotReload>true</WasmEnableHotReload>
</PropertyGroup>
The workflow is finally what it should be: edit code, save, see changes. The inner dev loop for Blazor WASM is now almost on par with what JavaScript frameworks have had for years.
Form Validation Source Generator
This one's subtle but important.
Before .NET 10: Validation used reflection. Every form submission, the runtime reflected over your model, found attributes, built validators. It worked, but it wasn't AOT-friendly. And it wasn't fast.
After .NET 10:
// Program.cs
builder.Services.AddValidation();
// Your model
[ValidatableType] // New: enables source-generated validators in .NET 10
public class OrderForm
{
[Required]
public string CustomerName { get; set; }
[Range(1, 100)]
public int Quantity { get; set; }
[ValidateComplexType] // Now works with the source generator for nested objects
public Address ShippingAddress { get; set; }
[ValidateEnumeratedItems] // Collection items validated via the generator
public List<LineItem> Items { get; set; }
}
The source generator runs at compile time. It creates optimized validators. No reflection at runtime. AOT-compatible. Faster. See Blazor forms and validation for implementation details.
And finally—finally—nested object validation just works. No more custom ValidationAttribute hacks.
404 Handling That Makes Sense
How did we handle 404s before?
@page "/products/{id}"
@if (_notFound)
{
<NotFoundPage />
}
else if (_loading)
{
<Loading />
}
else
{
<ProductDetails Product="_product" />
}
@code {
private bool _loading = true;
private bool _notFound = false;
private Product? _product;
protected override async Task OnInitializedAsync()
{
_product = await ProductService.GetById(Id);
_notFound = _product == null;
_loading = false;
}
}
Every component. Every page. Manual state tracking for "does this thing exist."
.NET 10:
@page "/products/{id}"
@inject NavigationManager Nav
@code {
private Product? _product;
protected override async Task OnInitializedAsync()
{
_product = await ProductService.GetById(Id);
if (_product is null)
{
Nav.NotFound(); // That's it. That's the whole thing.
return;
}
}
}
Works in SSR. Works in interactive mode. Works everywhere. The framework handles the 404 response, the page display, everything.
New templates even include a NotFound.razor component out of the box. Customize it once, use it everywhere.
Navigation Improvements
This one drove me crazy for years.
User is reading a long page. They click a filter that updates the query string. Blazor navigates to the "new" URL. Page scrolls to top. User loses their place. User curses your name.
Fixed in .NET 10. Same-page navigation no longer scrolls to top. Query string changes preserve viewport. Fragment changes work correctly.
Also:
<NavLink href="/products?category=electronics" Match="NavLinkMatch.All">
Electronics
</NavLink>
With NavLinkMatch.All, this stays active even when you add &sort=price to the URL. Query strings and fragments are ignored for matching purposes.
It's the little things.
Reconnection UI Updates
The default reconnection modal in Blazor Server has always looked... fine. Generic. Like it was designed by committee in 2019 (because it was).
.NET 10 templates include a new ReconnectModal component:
Components/
├── ReconnectModal.razor
├── ReconnectModal.razor.css # Collocated styles
└── ReconnectModal.razor.js # Collocated scripts
It's CSP-compliant. It's customizable. It actually looks like it belongs in your app.
And there's a new event to hook into:
document.addEventListener('components-reconnect-state-changed', (e) => {
console.log(`Reconnection state: ${e.detail.state}`);
// States: 'connecting', 'connected', 'failed', 'retrying' (new!)
});
The retrying state is new. Now you can show "Reconnection attempt 3 of 10..." instead of just spinning forever.
Passkey Authentication Support
ASP.NET Core Identity now supports WebAuthn/FIDO2. In English: fingerprint and face login. Hardware security keys. Phishing-resistant authentication.
// Program.cs
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3; // Enables passkeys
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
The Blazor Web App template scaffolds the passkey endpoints and UI—most of the work is configuration rather than hand-rolling WebAuthn:
- Register a passkey
- List registered passkeys
- Remove passkeys
- Login with passkey
- Passwordless account creation (yes, fully passwordless flows are supported)
No third-party libraries. No complex WebAuthn implementation. Just... it works. For implementation guidance, see Introduction to Identity on ASP.NET Core.
QuickGrid Updates
If you're using QuickGrid (and you should be—it's excellent), two quality-of-life improvements:
<QuickGrid Items="_orders" RowClass="@GetRowClass">
<PropertyColumn Property="@(o => o.Id)" />
<PropertyColumn Property="@(o => o.Status)" />
</QuickGrid>
@code {
private string GetRowClass(Order order) => order.Status switch
{
OrderStatus.Overdue => "row-danger",
OrderStatus.Pending => "row-warning",
_ => ""
};
}
Dynamic row styling based on data. Finally.
And for column options:
private async Task ApplyFilter()
{
// Apply your filter logic
await _grid.HideColumnOptionsAsync(); // Close the popup programmatically
}
Small things. Big productivity gains.
WebAssembly Improvements
Improved Service Validation
You know what's fun? Registering a service with the wrong lifetime. Then discovering it at runtime. In production. On a Friday.
// Misconfigured lifetimes are now caught with clear diagnostics
builder.Services.AddScoped<MySingletonDependency>(); // Oops, should be Singleton
builder.Services.AddSingleton<MyService>(); // Depends on scoped service
In .NET 10 WebAssembly, misconfigured lifetimes like a singleton depending on a scoped service are caught with clear diagnostics instead of blowing up at runtime. Your build pipeline catches it. Not your users.
Response Streaming by Default
var response = await Http.GetAsync("/api/large-dataset");
var stream = await response.Content.ReadAsStreamAsync();
// In .NET 10, this uses BrowserHttpReadStream automatically
// Memory efficient. No massive allocations.
Large file downloads no longer eat all your memory. The browser's streaming capabilities are used properly.
Performance Diagnostics
New diagnostic tools for WASM:
- CPU performance profiles
- Memory dumps
- Performance counters
- Native WebAssembly metrics
Finally, you can figure out why your app is slow instead of just knowing that it is. For more on Blazor WebAssembly, see ASP.NET Core Blazor WebAssembly.
Migration Checklist
Upgrading from .NET 9? Here's what to check:
- [ ] Update TFM to
net10.0 - [ ] Add
builder.Services.AddValidation()if using new validation - [ ] Review reconnection UI—new component is available but not auto-applied
- [ ] Test navigation behavior—same-page navigation no longer scrolls
- [ ] Consider
[PersistentState]to replace manual state persistence - [ ] Check JS interop—new APIs available for cleaner object handling
- [ ] Test passkey support if using Identity
Common Mistakes to Avoid
1. Overusing [PersistentState]
// Don't do this
[PersistentState]
public DateTime LastClick { get; set; } // Why are you persisting this?
[PersistentState]
public bool IsMenuOpen { get; set; } // UI state doesn't need persistence
Persist data. Not UI state. The serialization overhead isn't free.
2. Forgetting the New Validation Registration
// This won't work with [ValidatableType]
builder.Services.AddRazorComponents();
// You need this too
builder.Services.AddValidation();
The source generator creates the validators. AddValidation() registers them.
3. Assuming Passkeys Work Everywhere
// Check for support first
if (await PasskeyService.IsSupported())
{
// Show passkey options
}
else
{
// Fallback to password
}
Safari on old iOS? Some Android browsers? Corporate networks with weird policies? Passkey support varies. Always have a fallback.
4. Ignoring Circuit Pause/Resume
// The connection died, but you didn't pause first
// State is lost. User is sad.
// Do this instead
window.addEventListener('beforeunload', () => {
Blazor.pause();
});
Circuit state persistence only works if you actually tell Blazor to persist it.
When to Upgrade
| Situation | Recommendation |
|---|---|
| New project | Start with .NET 10. Obviously. |
| .NET 9, happy | Consider waiting for the first .NET 10 servicing update if you're conservative about upgrades. |
| .NET 9, hitting state issues | Upgrade now. [PersistentState] alone is worth it. |
| .NET 8 LTS | You have until November 2026. But 10 is also LTS. Plan the move. |
| .NET 6 or earlier | What are you doing? Upgrade yesterday. |
.NET 10 is an LTS release. Three years of support. This is the one to target for production apps.
Frequently Asked Questions
Is .NET 10 an LTS release?
Yes, .NET 10 is a Long-Term Support (LTS) release with three years of support from Microsoft. This makes it an ideal target for production applications that need stability and extended maintenance. Previous LTS releases were .NET 6 and .NET 8.
What's the biggest improvement in Blazor .NET 10?
The [PersistentState] attribute is arguably the most impactful change. It reduces 25+ lines of manual state serialization code to a single attribute, eliminating the double-render problem and redundant API calls during prerendering and hydration.
Does Blazor .NET 10 support passkeys?
Yes, ASP.NET Core Identity now includes built-in WebAuthn/FIDO2 support for passkey authentication. This enables fingerprint login, Face ID, and hardware security keys without requiring third-party libraries. The Blazor Web App template scaffolds the necessary endpoints and UI components.
How much smaller is the Blazor JavaScript bundle in .NET 10?
The blazor.web.js bundle was reduced from approximately 183 KB in .NET 9 to around 43 KB in .NET 10—roughly 76% smaller. Additionally, the boot manifest is now inlined, eliminating an extra HTTP request during startup.
Can I use Hot Reload with Blazor WebAssembly in .NET 10?
Yes, Hot Reload is now enabled by default for Blazor WebAssembly Debug builds. Edit a .razor file, save, and see changes instantly without rebuilding or refreshing the browser. For non-Debug configurations like Staging, you can enable it manually with <WasmEnableHotReload>true</WasmEnableHotReload>.
How do I migrate from .NET 9 to .NET 10?
Update your Target Framework Moniker (TFM) to net10.0, add builder.Services.AddValidation() if using the new source-generated validators, review the new reconnection UI component, and test navigation behavior changes. Consider replacing manual state persistence with the [PersistentState] attribute. See the Migration Checklist section for details.
Final Thoughts
Every Blazor release, I look for the improvements that make the development experience smoother and the rough edges fewer.
.NET 10 delivers. Substantially.
Blazor has been production-ready for years—plenty of enterprises have been running it successfully since 2019. But .NET 10 takes it further. The state management story is now elegant instead of just functional. Performance is excellent. The JavaScript interop feels modern. The developer experience has matured significantly.
Is it perfect? No. The debugging story still needs work. The component library ecosystem is still smaller than React's. Some edge cases in WASM still require workarounds.
But .NET 10 removes many of the caveats I used to mention when recommending Blazor. It's not just a solid choice for .NET teams—it's a genuinely competitive option against any modern web framework.
.NET 10 raised the bar.
About the Author
I'm Mashrul Haque, a Systems Architect with over 15 years of experience building enterprise applications with .NET, Blazor, ASP.NET Core, and SQL Server. I specialize in Azure cloud architecture, AI integration, and performance optimization.
When production catches fire at 2 AM, I'm the one they call.
- LinkedIn: Connect with me
- GitHub: mashrulhaque
- Twitter/X: @mashrulthunder
Follow me here on dev.to for more .NET and Blazor content
Top comments (0)