AMalexaka1.dev

Blazor sucks

Or why having a brain aneurysm is not worth it just to avoid learning React

By Alex Martossyon

Writing Blazor for my full-time job feels like watching someone trying to convince a homeless person that dryer lint is candy.

Blazor is being sold as a full-stack framework that lets you move fast.

Front-end web development made easy
Whether you're an individual or team, build secure, full-featured web apps faster with fewer resources.

One stack
Use the power of C# and the richness of the .NET platform to build full-stack web apps with greater productivity and performance.

According to Daniel Roth: Blazor is good for B2B or internal apps, where you don't need to worry about performance or browser compatibility. I am paraphrasing, however, I think Blazor sucks even in those scenarios, and your co-workers don't deserve an inferior experience just because they are not paying you.

But first, I want to get my biases out of the way. My job for the past 6 months has been to work on a greenfield .NET 9 Blazor project, and will be for the foreseeable future. However, I also have extensive experience with Angular and/or React (same thing). For backend, I love ASP.NET Core. This is partly why I have this bitter feeling when it comes to using Blazor.

Pros and cons

ProsCons
C#The 3 render modes all suck
Arguably better than MVC or Razor pages, when static renderingRazor templating is still very rudimentary
6 developers are working on Blazor at Microsoft
WASM for UI is an evolutionary dead-end
Javascript is still unavoidable, but 10x more harder to use
Authentication is a nightmare to set up

The 3 render modes all suck

Blazor is actually handy for a fully static, server-rendered app, with no interactivity. That workflow has a much nicer DX than what we had in MVC or Razor Pages. This is mainly because you can finally define reusable component fragments and not just full pages, or without the awful partial views.

The problems start coming as you move from Hello world! to more complex apps.

If using any two out of the three render modes, your app's complexity grows exponentially and the two modes leak into each other in the most disgusting sneaky ways possible. And I argue that you really can't pick just one of them and call it a day.

Static render mode / pre-rendering

Static render mode has been around forever. It is what PHP does, it is index.html, elementary. A request hits the server, it renders HTML and returns it as a response. In ASP.NET Core terminology this means it is part of the classic HTTP middleware pipeline. More on this later.

The bane of all SPA apps is the initial load time. To solve this, pre-rendering was (re)invented. In Blazor, pre-rendering and static rendering are really the same under the hood, it's just that interactive components by default pre-render on the server, and hydrate on the client.

When a user navigates to the app, the server renders some HTML, returns it, the user essentially sees the app instantly, and then the page hydrates all the JS and dependencies in the background. This feels fast, even though the actual time to interactivity is roughly the same. For this very reason pre-rendering is unavoidable in Blazor if we want the users to feel the app is fast.

Interactive server

One of the ways Blazor achieves interactivity is by opening a Signalr connection to the server from the browser, and sending chunks of data back and forth, so the blazor.js can handle user interactions on the client. This has the nice benefit of all the business logic remaining on the server, under our control as is with traditional web apps.

The issue with this is that now just for having a single button that increments a counter you have to have an open websocket to your server at all times. This gets out of control fast when your infrastructure is not a single instance 5$ VPS. For example, Azure app service has a 50k limitation on open websockets per instance. The problem really starts rolling when you discover opening new tabs also open separate websocket connections (which is a browser security feature). And while you can prevent a desktop app from opening multiple times by simply reading the process table, you can't and shouldn't prevent the user from opening new tabs. But let's say you want to prevent the user to open new tabs. Congratulations, you now get to spend weeks engineering a layer on top of signalr that prevents new connections from being established to an authenticated user, to reduce the load.

TL;DR is that using this render mode literally requires special infrastructure (dynamic scaling, load-balancing, signalr backbone, etc.). In my mind there is no world where it is justifiable to have a websocket connection open at all times just to render a button with a click handler.

Not to mention websockets is kind of an abandoned technology in the browser. And also the most trivial-est of trivial tasks are near impossible to solve, and are left as an exercise for the reader to implement.

WebAssembly

WebAssembly kinda sucks in the browser. WebAssembly CAN be very useful. For example, running SQLite in WASM and then calling it from JavaScript is handy for a local first app. Or counting the Fibonacci sequence externally with WASM and just awaiting a promise on the JS side is orders of magnitudes faster than doing the whole thing in JS. Although it's not WASM that is necessarily fast, it's the language that was compiled to WASM. C++ or Rust WASM modules will be inherently fast. While the .NET runtime has improved massively over the last couple of years, .NET is still a garbage-collected, just-in-time compiled language that needs a runtime before it can run on your machine. This is true for the WASM target too. Which means the JIT is happening in the browser! So you have to download the full .NET runtime compiled to WASM to run a hello world app in the browser. We're talking about megabytes of WASM compressed. Compared to Next.js, that's only hundreds of kilobytes uncompressed. And that runs natively in the browser since it's just JavaScript. Not to mention in JS land, tree shaking is a solved problem. In WASM land, it's up to the runtime to do it. In .NET Ahead-of-time compilation is just starting to really take off (which really just means no JIT-ing), trimming has great support now in the desktop runtime. But WASM? It is still a mystery to me what these options actually do, but this guide also states this actually increases the bundle size, since the code won't JIT, so we have to transfer IL over the wire.

Not to mention, cloud has egress costs. CDNs aren't free either. So once again, Blazor forces you to have special infrastructure. Suddenly you get massive pressure to set up a complex CDN in front of your app to cache all those wasm binaries and deliver them to your users instead of directly from your cloud provider. The I just want to write C# target audience better dust off their devops knowledge too.

One other thing is that WASM for UI kinda sucks. Wasm does not have direct access to the DOM. All interactions must go through JavaScript bridges or APIs, which adds overhead and complexity. Every time Wasm code needs to interact with browser APIs (like the DOM, events, or even basic things like alert()), it must call out to JavaScript. The JavaScript ecosystem has decades of mature, well-supported UI libraries and frameworks. There has been incredible amounts research that has gone into the ReactDOM package in particular. With WASM, as far as I know, the rendering first happens in WASM, then it gets serialized as text to the JS layer, which then does the actual DOM replacement. WASM could not be any more steps removed from the browser even if it was running as a separate app on my machine.

Auto render mode

.NET 8 introduced the InteractiveAuto render mode which is now the intended default for interactivity (Microsoft claims it's not, but it is very much the default in every template). This is a render mode that first starts out as InteractiveServer and in the background starts downloading the WASM files. This means the user has a very short time to interactivity, and while the browser downloads and parses the wasm, they get to use the app. Once WASM is on the client and the user navigates to a new route (or revisits the app), the framework switches over to the client code and (hopefully) terminates the websocket since it is no longer necessary.

The irony of Blazor is that the best render mode is quite literally the worst of all worlds. Because you still need the special infra for the websockets, and you also need the CDN to cache the megabytes of WASM files you'll serve to your client. Just for the bare minimum user experience of having a functioning app without seconds of load times.

So why do all of them suck?

Dependency injection

Blazor absolutely shits into .NET's dependency injection framework. Blazor is a completely different architectural paradigm that in my opinion, is orders of magnitudes harder to grasp for a non-frontend dev, who just knows C# than if you just taught them React for a week. If you thought that Next.js server actions have gotchas, you haven't seen nothing yet. (And I'm not talking about having to wire up dependencies twice in both the client and server projects, I can live with that.)

Static render mode

Static render mode plays nicely with the classic ASP.NET Core experience. (Well sort of, but that's in the authentication section.) A request is a scope. Scoped dependencies are resolved. The request moves from middleware-to-middleware to build up the context. Who is the user, which tenant they belong to, what they have access to etc. Very simple.

Interactive Server mode

Interactive Server, on the other hand, is based on Signalr, which means once the circuit opens, then that whole circuit becomes the scope. Except that it doesn't. You have to set up a special hack to access scoped services in server side (non-component) services. This sneaks in the service locator pattern into your codebase, and you cannot get rid of it. ANY code that MAY be used by both the static render pipeline and the interactive server pipeline, means you will have to use this special service locator to resolve your dependencies. This increases the complexity of your code because it hides the real dependency tree and makes testing harder. This is also a leaky abstraction that can spread to your whole codebase if you don't actively fight it with the business end of a tea kettle and avoid as many scenarios as possible.

In case you're wondering which end is the business end of a tea kettle. The answer is all of them if you throw it hard enough.

WebAssembly mode

In WebAssembly the scope is the full webassembly app itself. So every scoped service in webassembly is a singleton since the app starts and creates the services and a new scope is never created in its lifetime. What this means in practice is that if you register a scoped service, it will fail to resolve, so you have to register them as singletons or transient. This is actually fairly straightforward, because the Client code is a fully separate project, with its own bootstrapping code, but it's yet another rake you can step into.

It's actually still not this simple

All of the above is true; however, there is one extra spin. The individual components in Blazor (*.razor files) while share the same scope, are rendered concurrently. This is a classic pitfall with tools like Entity Framework Core, which is not thread safe, so a single instance cannot be used concurrently. Once again, you are forced to write your entire app to use the factory pattern and create an instance for each component you use it in, which adds mental overhead because:

  1. you have to remember to do this
  2. you are forced to engineer over Blazor and spend time building these workarounds instead of your app
So what does this all mean?

It means that for every line of code you write you either:

  • Have to be aware which parts of the app will use it
    • and remember to prevent it from being used elsewhere
  • Have to write code that is DI lifecycle agnostic, which either:
    • forces you into antipatterns,
    • or just simply adds a minimum 2x cost to every single line of code you produce.

This is a surprising level of complexity for the Front-end web development made easy with C# evangelists.

Authentication

After recovering from the sweet embrace of a brain hemorrhage, I was able to sort of figure out how one would need to do authentication to a Blazor app.

I was going to do an entire section on how setting up authentication is a nightmare in Blazor, since none of your experience with dotnet is applicable because of the crazy complexity of the paradigms. But I just don't care anymore.

Sorry! Maybe next time.

JavaScript Interop not only sucks, but it is literally unavoidable

The browser does not allow WASM to access any of the usual suspect resources that a simple js app would be able to. This means that if we want to access like the clipboard, we need JavaScript to handle that and somehow pass this data to Blazor. This means we are FORCED to use an interactive render mode (so special infrastructure).

Once again, the complete irony of Blazor is that it sells you the utopia of Writing all of your code in C# and Razor, so that you don't need JavaScript. Only to then force you to use JS anyway for the most trivial of things. Clipboard, localstorage, you name it.

The actual mechanism for JS interop is not horrible, although JS to .NET is kinda disgusting, but this is not that different from any kind of FFI boundary with other languages. For the JS side it relies heavily on ESM, which is the correct choice at least.

We can't forget one important fact, though. The browser runs JavaScript. The web was built on JavaScript. If the library you want to use does not have a Blazor wrapper already, then congratulations, you get to spend weeks at best (months at worst) writing a library that is not your app. If you use React, then literally everything has a React wrapper already. Blazor is sold as being able to iterate fast for devs who know C# but don't know JS. But this is just not true. Blazor does not have the vast library support that JS apps do, and even the least complex apps out there will need to use at least some of these or spend time developing their own. If you were an R&D company, it would make sense to invest in making your own libraries, but most of you are just making a CRUD app that connects various services.

And to add insult to injury, this is what Microsoft claims on the blazor website:

Enhance your Blazor experience with great tooling
Leverage rich tooling for Blazor in Visual Studio Code and Visual Studio to get started fast using built-in project templates and scaffolding, hot reload, and AI-powered code editing.

I honestly don't know what tooling they are talking about. It is clear Microsoft has never once made an app with Vite, because to claim Blazor has hot reload is admitting that you are under the influence of creativity supplements.

But if you do want to create a wrapper, because dotnet is not in the JS ecosystem, it is 10x harder to integrate it with JS libraries. Thankfully, Vite can get us 80% of the way there, but the remaining 20% once again adds such complexity that you're not working on your app, but building out these libraries and integrations. There is a non-zero chance that I will have to just set up a Vite project and try to integrate it into our Blazor app. While maintaining the compression and integrity features that .NET 9 shipped with.

Razor templating vs. JSX

According to some, React won and by extension JSX won. We can argue all day, about Angular, Svelte, Vue, or even HTMX. But I think JSX is the superior templating language. The massive adoption of React forced them to hyper fine-tune the tooling layer of the language.

The unquestionable benefit of JSX is that you are JUST writing JavaScript that runs from top to bottom. The JSX is within your JS code. With Razor it's the opposite. You are always writing Razor, and the internals of Razor itself are something you would see in a Clan Moulder propaganda video. It is an eccentric mutation of C#, HTML, and inline C# like templating. The different parts of it are compiled differently. A Razor file is actually source-generated into many smaller files.

Another shortcoming of Razor is that right now you cannot mix C# and HTML attributes. What this means is that you can write something like <MyComponent data-id="@(MyCsharpVariable) and some free text">some text</MyComponent>. However, when you introduce properties that are not HTML, but Razor, this fails to compile. So if MyComponent has a parameter for the C# code, then the syntax you use, has to change to this: <MyComponent SomeProperty="@SomeVariable" data-id="@($"{MyCsharpVariable} and some free text")">some text</MyComponent>. This is completely arbitrary and shows the shortcomings of the language.

Here's the fun and/or sad part. Razor is the be all end-all solution of .NET for a frontend templating language. While ReactDOM is a library built on top of JSX. In React, you can do the spread attributes ({...props}) syntax. Which you can also do in Razor although the syntax is hideous comparatively. However, unlike in Razor, you can strongly type props in React. For example, my button component can be typed to only accept button legal attributes from the caller, like onclick, id etc., but NOT href or referrer. This means that at coding time I can enforce type safety by not misusing my components. In Blazor, you cannot do that, you can only blanket forward additional attributes to a component down the chain, which means you get zero type inference at the caller level. This is just one example, but these DX wins/losses add up.

The so-called strongly typed language benefits of C# completely fall apart and are put to shame by — of all things — JavaScript.

Microsoft manages to never use its own UI frameworks

The real metric to see if a Microsoft technology has a future is to see if they themselves use it. YARP, for example, is pretty good, and Azure App Service uses YARP as its proxy.

Blazor on the other hand, has an uncertain future. Blazor is in the unfortunate downward spiral of:

  • Adoption is low
  • Engineering investment is reduced
  • Sentiment goes down
  • Adoption goes lower
  • Repeat

They claim that the numbers are going up year over year. However, I would be curious to see what the retention is on Blazor. Really any kind of frontend framework vendor locks you into them. I don't doubt Blazor is popular in the I haven't used it yet, so let's try it out crowd. And I believe some enterprises adopt it for their apps. But I want to see how many of them totally regret picking Blazor and wish it wasn't that expensive to rewrite it in anything else.

Compare this to Bun, which is disrupting the NodeJS ecosystem and is skyrocketing to replace it in almost all real world scenarios. It receives heavy criticism from the live by the Node, die by the Node community, but I think the tool speaks for itself, and it continues to prioritize good DX over the design by committee: try to appease everyone and end up angering everyone competitor.

Microsoft Teams also famously went under a rewrite a few years ago, and Microsoft somehow managed to not use any of the 5+ UI frameworks they were still actively developing at the time, and went with React. I hate Teams as much as the next person, but React is not the reason why Teams sucks.

Blazor is part of dotnet, which is not good enough for the web

.NET has a very specific enterprise-y support cycle. But for the web frontend, this is not good enough. Waiting 2+ years for features on the frontend is an eternity. The web moves very fast, and Blazor will never be able to keep up unless development picks up and adoption goes much higher. If you're missing a feature in Blazor, your BEST chance is to wait for a full year until it's added in the next release. If you file a bug, and it's fixed, it is not backported to the currently supported release. If you're missing a feature in Next.js, you can bully the CEO personally on X and have it added.

According to the principal product manager of ASP.NET Core, Daniel Roth, Blazor has six active developers on it. That means that the three frontend developers at my work writing the Blazor app are 50% of the allocation that a Fortune 500 company is able to afford on the technology they push as the future of .NET frontend. On the face of this, it would be fine, but teams of smaller sizes are able to iterate much faster on comparable frameworks without the backing of Microsoft.

The aspnetcore repo on GitHub has about 1300 contributors. Let's say 20% (260) of these contributors are for the Blazor side of things (and this is unrealistically generous). Compare that to Next.js's 3500, React's 1700, Django has 2600, or even Laravel has 600 contributors. The numbers speak for themselves, adoption is much greater for these other frameworks. Not to mention by extension the tooling surrounding them.

Missing tooling

Component libraries

Blazor has pretty much 8 UI libraries to choose from. None of them are headless. This is important because headless (unstyled) UI libraries are the future. Even the famous MUI library has started to work on a headless solution.

All of these come with their own styles, which is fine if you don't need a company image. At work, we chose Material 3 as the design system. None of the component libraries support this theme for free. However, even if we paid for one, the current UX design has a bunch of customizations made to Google's designs, so we would have to reverse-engineer the components anyway and restyle them. This is still true at the time of writing.

Whereas for something like React, you have Headless UI, Radix, and now Base UI (from MUI). All of these can be styled however you want, which would be perfect for us. Sadly, no such library exists that we are confident to pick.

React also has very extensible component libraries such as Shadcn UI. The original website and this neobrutalist theme uses the exact same components at the core, but looks incomparable in style.

But even if we had Shadcn in Blazor, the way Blazor works is it would require interactive render mode, which again brings all its drawbacks with little benefits.

The drawback of these Blazor component libraries is that they also aren't composable. This is a very niche topic, but the real power of tailwind is that it makes creating composable UIs (like ShadCN) really easy. But it requires a mental paradigm shift, which is tough for developers to let go. These component libraries are at least a decade behind the current front end trend, so their rigidity is not very fun to interact with.

Routing

Routing in ASP.NET Core is still very much free text-based. The IDEs are doing their best to have some hints, but from my experience this does not work for Blazor @page attributes. Whereas in React we have a very nice solution to this powered by TypeScript.

State management

State management is basically non-existent in Blazor. This is being worked on with .NET 10, but nowhere near the feature set of tools like Redux toolkit, Convex, or even TanStack Query.

Other grievances

Blazor WASM really sucks. It has no multithreading. Which means some libraries that rely on separate threads just straight up crash your app at worst, and just not work at best. Betting on WASM is betting on a technology that may or may not be delivered. If we were an R&D company like Vercel, this would make sense, but as an ERP Saas, it makes no sense to bet on this technology in 2025. The WASM side of Blazor is completely at the mercy of the runtime team which is not a good look, when you

just wanna build your app and not worry about all these UI frameworks in JavaScript.

Opinion

Blazor is a good solution if the current year is 2014 (2 years before the release of .NET Core). Web UI development has evolved a lot since then, and it is currently living its Renaissance. Yes, learning React is a cost, but I argue the cost-benefit is heavily against Blazor, because the initial oh I can easily render html wears off once you add auth, state management, external services, components, or any medium complexity feature. None of the render modes work for you, but against you. Whereas React is very, very mature, has amazing tooling, and is straightforward to pick up. And you may say, okay but our team already knows C#, however for the reasons listed above, Blazor itself greatly offsets this benefit. Learning the specifics of Blazor will be harder for C# devs than just learning React.

The argument that it's good enough for internal or B2B apps sounds like a dog whistle for your co-workers don't deserve to have a good developer experience, nor user experience when interacting/developing with a front-end application.

...

This is my very first article, so bear with me. ❤️
Feel free to call me out on things I said, but go easy on my delivery as I have no writing experience, so I may not have conveyed my arguments coherently.