DEV Community

Cover image for Blazor in .NET 10: What's New and Why It Finally Feels Complete
Mashrul Haque
Mashrul Haque

Posted on • Edited on

Blazor in .NET 10: What's New and Why It Finally Feels Complete

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

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
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!)
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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",
        _ => ""
    };
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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.

Follow me here on dev.to for more .NET and Blazor content

Top comments (0)