In 2021, the same year I wrote up why we'd bet on Blazor WASM for Powered4.tv, Microsoft put the site on the .NET Conf stage.

Powered4.tv on the .NET Conf stage, live on Blazor and .NET 6 release candidates: 5.6M video impressions, viewers in 65 countries.
Powered4.tv was "The Netflix of wrestling." A small team of four, shipping in our evenings and weekends. The business sadly wound down under commercial pressure, but the Blazor bet was the part that held - we'd chosen it because it aligned with our core strengths, even as I admitted I'd worried it "would be okay for an internal business application but perhaps not ready for a public facing site."
Five Years Later
Five years on, I'm building MinCalc on my own: a daily-puzzle site with five small games, a fresh puzzle every day. The bar is higher than Powered4's ever was. I've walked into a crowded room dominated by NYT Games, Wordle and LinkedIn's puzzles, plus tonnes of new games [see a site like listdle], and now an onslaught of copy-paste clones and AI-generated filler running the same handful of mechanics, badly.
There's no winning that on volume. The only way through is to build something calmer than the big players and more careful than the clones. To be successful I need:
- fast on first load;
- indexable by every crawler;
- pretty on mobile.
That middle one no longer means just Google. Plenty of players now ask ChatGPT instead of searching, and those tools keep their own indexes, so I send the sitemap to several providers rather than Google alone.
It's the exact public-facing site 2021-me was nervous about, built solo, with less time than four of us had. So I bet on Blazor again, with even more conviction... let's find out why, and where it's still limited.
The Pain Points (June 2021 Review, Re-Graded)
They're all resolved or routed around. Some of it is the framework maturing; some is per-component render modes (more below) letting me pick the right tool per route. The 2021 checklist, re-graded:
Performance
My biggest performance worry has always been SEO. We all know Blazor WASM is slower than pure JS - it has to download the interpreter and load the application first.
This is the one I most expected to still be a wart in 2026.
It isn't. At least, not where it matters.
31 of MinCalc's 43 routes are zero-JavaScript Static SSR, and they score Lighthouse 100. LCP 1.3 s. Total Blocking Time 0. CLS 0. Every SEO surface is in that set: /about, the blog and its posts, every how-to-play page, every hints article, every archive month.

PageSpeed Insights, mobile, /about - one of the 31 Static SSR routes.
That 100 takes PSI a couple of cold, throttled runs to settle on, mind; on mobile it flickers in the 90s before it lands, while local Lighthouse just sits at 100. The metrics underneath are the steady part [LCP 1.3 s, TBT 0, CLS 0]; the headline number wobbles run to run, the way everyone's does. Open the live runs yourself: /about and /quackle/how-to-play.
Either way, it's the perf I would have laughed at as "obviously not Blazor" in 2021 - on real public pages, with zero client island.
The other 12 routes are interactive WASM - the five games, each playable today or by date, plus the home grid. They sit at perf 70-ish. That's the WASM cold-start tax, which I get into below. It hasn't gone away. But the trick that made the gap acceptable is the new beat further down.
Pre-Rendering
Verdict: solved in .NET 10. In 2021 I wrote that pre-rendering "left a weird page flash which was not a nice experience for customers, everybody commented on it." I called .NET 6 preview 2 a fix. It was. Partially.
And the flash has been fixable for years. The framework has long let you carry state across the prerender boundary by hand - but it was hoops: stand up a state service, register a save callback, serialise each value into it, manage the cleanup, and pick a stable key yourself. Enough ceremony that skipping it was tempting, and skipping it brought the flash straight back.
.NET 10 collapses the lot into one attribute, [PersistentState]. Mark a property; the framework serialises it server-side, embeds it in the prerendered HTML, and rehydrates it when WASM mounts. No re-fetch. No flash.
The docs show it off with a counter. That's not the interesting bit. Here's what MinCalc actually hands across - the puzzle itself:
[PersistentState]
public TarglePuzzle? PersistedPuzzle
{
get => Today?.Puzzle;
set => _restoredPuzzle = value;
}
The server picks today's puzzle, prerenders the board, and tucks the puzzle into the page. WASM mounts, reads it straight back, carries on. No flash of an empty grid, and no arguing with the server about which day it is.
The bit I like: on the canonical /targle route the client never calls the seeds API on first paint, because the puzzle already arrived in persisted state. The pool [api/v1/seeds/<game>, Brotli'd] only warms in the background afterwards, seeding past dates and other games, which keeps that network cost off the critical path.
I no longer need to deal with PersistentComponentState injection, a RegisterOnPersisting callback, or any IDisposable plumbing! The weird page flash everybody commented on in 2021 is completely gone.
Head Elements
Verdict: first-class now, no library. In 2021 I named the missing-<head> gap and recommended Toolbelt.Blazor.HeadElement as a workaround. First-class now:
<HeadContent>
<title>@Title</title>
<meta name="description" content="@Description" />
<link rel="canonical" href="@Canonical" />
</HeadContent>
The bet I made in 2021 (that this would be solved) is solved. And it's solved in the obvious way that doesn't need a community library.
Still Solid, No Notes
Two 2021 wins need no re-grade. CSS/JS isolation is still mature and still works. And Hot Reload - dotnet watch, which 2021-me called "ridiculously fast!!!" - is still my daily driver for every session; the three exclamation marks still apply.
Testing
This section has matured a lot.
In 2021 I named bUnit, Verify, and Playwright as "the same testing strategies that any modern UI developer would expect." Still true. But the bigger beat now is that WebApplicationFactory supports Blazor.
In .NET, a single Program.cs hosting both server-side rendering and InteractiveWebAssemblyRenderMode lets you spin up the entire app in-process from xUnit using WebApplicationFactory<Server.Program>. That means:
- Full ASP.NET Core integration tests against the real DI container
- DI overrides for testing (
ConfigureTestServicesto swapIClockfor a fake, swapIPuzzleProviderfor deterministic seeds) -
Playwright against in-process Kestrel, with no separate
dotnet runprocess, no port management, no "did the server boot yet" flake loop
This is bigger than the convenience: the whole integration-test surface that used to need docker-compose or external orchestration now runs in-process. I can check the full application for regressions with deterministic tests in the CI/CD pipeline, with no external dependencies and minimal effort.
Download Size
This is still a real consideration, and bigger than the minimal JS app. Two things changed how I think about it.
- It stopped being one number for the whole site. Render modes mean the WASM payload only loads on routes that actually need it. The SEO surfaces ship zero JavaScript, so for the pages search engines and first-time readers hit, the download is 0 bytes. The interactive games carry the runtime, and nothing else does.
- I get to decide where the bytes live. When I moved the blog, about, archive and how-to page components out of the client project and into the server, MinCalc's interactive payload dropped from 272 KB to 226 KB Brotli. Same .NET, same trim settings. Those components just stopped compiling into the WASM assembly, because they render server-side now.
The thing that should have shrunk it years earlier was me paying attention. The build-time CSS/JS minifier I lean on (NUglify) had been quietly riding along in _framework/, shipped to every browser, and I only caught it when I finally opened the network tab and found ~150 KB of build tooling that had no business being there. One ExcludeAssets="runtime" and it was gone. None of that is a Blazor story. It is the older, duller lesson that "it builds and runs" is not the same as "I know what I'm shipping" - and I'd shipped it to real users for months.
So the 2021 cover-story ("we decided a slightly larger download is acceptable") is no longer the only answer I have. Now I can decide per route, and for the routes that matter most, Blazor's download size is 0 bytes.
The New Beat: Per-Route Render Modes
This didn't exist in 2021. It's the single biggest reason I'd bet on Blazor harder in 2026 than I did in 2021.
In a Blazor Web App you can pick the render mode per component, not per app. The options:
- Static SSR: server renders HTML, ships zero JavaScript. Perfect for SEO surfaces.
- InteractiveServer: server render with WebSocket-driven updates. Low payload, latency-sensitive.
- InteractiveWebAssembly: full client-side WASM. The classic Blazor model.
- InteractiveAuto: server first, WASM once the runtime downloads. Best of both, at a complexity cost.
MinCalc uses two of these. Static SSR for everything that doesn't need client interactivity (article pages, archives, how-to-play, blog). InteractiveWebAssembly for the games themselves, where you need keyboard handling, smooth animations, and persisted state. 31 routes to 12, Lighthouse 100 against not-100: the split mirrors the user need exactly.
This is the answer to the 2021 anxiety. "Public facing might be too slow" assumed Blazor was one global choice; in 2026 it isn't. Same codebase, same components, same DI.
Wiring the Two Modes Together
MinCalc is a hosted Blazor app - two projects: a server project (the ASP.NET host) and a client project (the WASM app). So when you hit /about or /targle, something has to decide: server-rendered HTML, or interactive WASM?
The server decides, per request. One server-side root - App.razor, in the server project - handles every request; even /targle is prerendered there before WASM takes over. Whether a page becomes interactive is just whether the server attaches a WASM render mode to it:
private IComponentRenderMode? PageRenderMode
=> HttpContext?.AcceptsInteractiveRouting() == true ? WasmPrerender : null;
AcceptsInteractiveRouting() and [ExcludeFromInteractiveRouting] are both built-in. With the attribute, a page renders as plain static HTML and no WASM is booted; without it, it gets interactive WASM. One flag per page, one decision in App.razor.
Getting those static pages to resolve took a beat longer than I expected, because there are two routers. ASP.NET endpoint routing, on the server, knows every page. But the <Router> component that maps a URL to something to render lives in the client project, so its table only holds the client's routes. When I moved the blog and how-to pages into the server project, that component stopped knowing they existed.
The bridge is one line. On a server-rendered request, App.razor lends the server's own assembly to that client-side Router so it can resolve the specific server page. It only does this on static renders:
<Routes @rendermode="@PageRenderMode"
AdditionalAssemblies="@(PageRenderMode is null ? ServerAssembly : null)" />
On an interactive render it's null - the page runs in the browser, which can't load a server DLL anyway. Nothing is lazy-loaded; those pages render server-side and arrive as HTML.
One sharp edge is worth knowing. That client Router only ever matches its own table, so a programmatic NavigateTo to a server-only route renders NotFound rather than reaching the server. Plain links are fine: a same-tab <a href> to a static page does a normal full-page navigation and the server renders it. (Even that had a framework wrinkle going from an interactive page to a static one; see below.)
What's Still Hairy
Five years and Blazor isn't perfect. Here's what genuinely cost me a debugging hour or two:
-
WASM cold-start on Interactive routes: first visit pays a download + runtime-startup tax. Lighthouse perf 70-ish on
/targlereflects it. Per-route render modes route around it for SEO surfaces but you can't engineer it away on the interactive surfaces themselves. Caches well after the first visit. -
Cross-mode anchor clicks: clicking
<a href="/about">on an Interactive WASM page can silently drop if the destination is Static SSR. This isdotnet/aspnetcore#64541. Workaround: wrap with<SsrLink>(or settarget="_top"directly). Fine once you know it; mystifying the first time. -
Globalisation / timezone trade-offs from the payload optimisations:
BlazorEnableTimeZoneSupport=false+InvariantGlobalization=trueshave hundreds of KB off the WASM payload. Real money. But they're trade-offs: noTimeZoneInfolookups, no culture-specific formatting. I get away with it because MinCalc is en-GB only with a single UTC daily reset. A product that ships in multiple languages and time zones would hit this hard. [Other globalisation strategies are available.]
Three warts in five years of shipping. Compared to the 2021 list (half of which the framework just fixed), that's a solid trajectory.
The Road Forward
The 2021 piece closed with "look out for a future post where we turn AOT on and compare differences." In keeping with the format, here's the 2026 forward-look.
The WASM runtime today is still Mono-based. That's the IL interpreter performance has been built around for five years. There's ongoing investment to converge the WASM runtime on the standard CoreCLR runtime instead, which would narrow the gap between server-side .NET performance characteristics and client-side WASM. If that lands in .NET 11, the startup-perf tax (the biggest remaining wart on Interactive WASM routes) could close meaningfully. I'll be running Lighthouse against the day-1 .NET 11 preview when it drops. 👀
In an Agentic World, Choose What You Know
Here's the section that doesn't exist in the 2021 article, because none of this existed in 2021.
The counter-argument I'd make against myself in 2026: "you should pick JavaScript/React. The training data is bigger, agentic tools generate more idiomatic React than Blazor, the ecosystem moves faster, and AI helps level the playing field for stacks you don't know deeply."
I think that argument is wrong. And the better agentic tooling gets, the more wrong it becomes.
Here's why.
Agentic coding doesn't replace your judgement. It multiplies it. So choose a stack where your judgement is sharp.
When Claude (or Copilot, or Cursor) writes 80 lines of code, you have to read every one. If those 80 lines are idiomatic C# and I'm a C# engineer, I spot the wrong-allocation pattern, the missing ConfigureAwait, the swallowed exception. In seconds. If those 80 lines are React with a useEffect dependency array that's quietly wrong, and I'm not a React engineer, the bug ships. The expertise bar didn't move; the throughput did. I still have to review every line that came out.
And the gap only widens over time. Agents keep pulling in dependencies you didn't choose, transpiling through toolchains you didn't configure, leaning on framework idioms you've never debugged at 11pm. The value of knowing your stack cold compounds with every line the agent writes - and C# + .NET, for a backend-leaning engineer, is exactly that. Familiar tooling, no transpile chain, IL you can decompile when something genuinely weird is happening.
There's a longer arc, too. Agentic coding is fastest at the spew phase - bash out a feature, clone a game by lunchtime, get something on the screen. The first draft was never the hard bit. The next fifty changes are:
- iterating without regressions;
- keeping the code clean enough that you can still navigate it three months in;
- catching the bug the agent quietly introduced because it didn't know about a constraint elsewhere in the project.
You're not the typist any more. You're the lead engineer. Directing the work, owning the result, making the codebase yours rather than the agent's. A repo full of agent-spew that nobody understands won't survive its second feature.
2021-me chose Blazor for developer productivity in a 4-person team with limited evenings. 2026-me would choose Blazor more strongly because agentic tooling, applied to a stack I know to the bytecode, is the highest-leverage way to ship as a one-person team. The trap of the agentic era isn't "AI writes bad code." It's "AI writes code in someone else's stack and you can't tell whether it's any good."
If you're a .NET engineer eyeing a jump to JS for the AI-tooling reasons, I'd push back. Stay where your judgement is sharpest and let the agent be the throughput multiplier.
Summary: Was Blazor the Correct Choice?
Five years on, two side projects in, and I'd bet a third time.
The 2021 hedge ("perhaps not ready for a public facing site") was wrong. Blazor is ready. It's the stack I'd pick because the site is public-facing, because SEO matters, because I have less time as a solo founder than I did as a 4-person team, because agentic tooling makes familiar stacks compound harder than ever.
The trajectory's good, the framework keeps maturing, and the productivity-per-engineer-hour, for a .NET engineer, is genuinely hard to match.
Try it if you haven't.
Top comments (0)