The pitch for Blazor is that you write C# and stop context-switching. That part is true, and it is worth more than the benchmarks suggest — being able to share a validation type between the API and the form that submits it removes an entire category of drift.
The parts nobody puts in the pitch:
Render modes are the real API
Server, WebAssembly, auto — the choice looks like a deployment detail and behaves like an architectural one. Server-side gives you fast first paint and a persistent circuit that dies the moment the network hiccups. WebAssembly gives you an offline-capable app and a download size you will spend a sprint apologising for.
Pick deliberately, per component if the framework lets you. Picking by default means picking wrong somewhere.
The JS interop boundary is where time goes
Every “we’ll just use that one chart library” turns into a lifetime-management
exercise: dispose the module, dispose the .NET object reference, and remember
that IAsyncDisposable exists for a reason. It is not hard. It is just never
in the estimate.
StateHasChanged is not a suggestion
Anything that mutates state outside the component’s own event handlers — a timer, a message from a hub, a background task — needs to tell the renderer. Half the “Blazor is buggy” reports I have chased were state that changed correctly and simply never got drawn.
Verdict
For internal line-of-business work with a .NET backend already in place, it is genuinely good and I would choose it again. For a public marketing site where payload size is the product, I would not go near it.