We started building production Blazor apps in 2021. Five years later, Blazor is now our default for almost every line-of-business and admin-portal project we take on. We've shipped more than twenty Blazor systems into production across manufacturing, healthcare, finance, logistics, food, construction and field services — and a handful of internal products on top of that.
This is a write-up of the patterns that have held up across all of that, the things we tried that didn't, and the advice we'd give somebody starting their first Blazor project today.
Why Blazor, still
Five years on, the appeal hasn't really changed. For the kind of work we do — internal portals, line-of-business systems, admin tooling — Blazor lets a single team write one app in one language, with full type-safety from database to button, and ship it without a SPA build pipeline.
The argument against Blazor was always going to be one of three things: payload size, real-time interactivity, or the maturity of the surrounding ecosystem. Five years of production work has dissolved all three for the kind of apps we build. The remaining objection is "I haven't tried it yet," which is not a technical objection.
Server vs WASM: stop agonising, just pick Server
Every time we start a new client project, somebody on the team has the temptation to do the "Server vs WebAssembly" analysis again. Don't. For 90% of the work we do, Blazor Server is the right answer, and the analysis is a waste of time. Here is the version we wrote down so we'd stop having the conversation:
- Pick Server if: the app is for known users, behind a login, and reachable from the venues those users actually use. This describes nearly every line-of-business and admin tool.
- Pick WASM if: the app is for the general public, must work offline, or has truly compute-heavy client-side logic.
That's it. We've shipped Server apps with 40 concurrent users sustained, 140 advisers in three offices, 400+ retail tenants — none of them needed WASM. The latency cost of SignalR is below human perception over a normal connection, and the bandwidth cost is so low it's not worth measuring.
We do use WASM, but mostly for our in-house Shopworx product, where the multi-tenant SaaS model and the variable connection quality of small retailers made the offline-first story worth the extra cost. Server is the default; WASM is the exception.
The "render once, hold the state" trap
The single biggest mistake we made early on was treating Blazor pages like Razor pages. A Razor page renders, sends HTML, and forgets. A Blazor page renders, holds state on the server, and re-renders every time that state changes. Those two things look the same in the editor and behave completely differently in production.
The first time it bit us was a portal where each user's session was holding around 20MB of unnecessary objects, because we'd put them on the component as fields and never cleared them. Forty users, 800MB of server memory, no idea why the App Service was OOM'ing.
The rules we now stick to:
- Components hold view state and nothing else. Selected ID, expanded panels, in-flight form values. That's it.
- Domain data lives in scoped services, not on the component. The service can decide to cache or not; the component just reads from it.
- Big lists are virtualised with
<Virtualize>. Always. No exceptions for "small for now." - Dispose anything that subscribes. If you
+=an event handler, you-=it inDispose, every time. Memory leaks via subscriptions are the most common Blazor performance bug we see.
[YOU: anecdote — the warehouse project hit a similar scale problem at one point if you remember the details, or there's a better one from manufacturing/finance. The point is: real production memory bug from forgetting Blazor's stateful nature.]
State management: keep it boring
Every Blazor team eventually argues about state management. Fluxor, Blazor-State, custom mediators, observer patterns, signals. We've tried several. The boring answer wins every time.
What we do now: a scoped service per area of concern (auth, current tenant, current selection, in-flight chat), each holding plain properties and exposing an event Action OnChange. Components inherit from a base class that subscribes on init and calls StateHasChanged on the event. That's the whole system.
This is unfashionable. It's also debuggable, type-safe, has no DSL to learn, and survives every .NET version upgrade with zero changes. The opportunity cost of "proper" state management — onboarding time, debugging time, version upgrade pain — has not been worth the marginal benefits in any project we've shipped.
Forms: write your own field components, once, and reuse them everywhere
Every Blazor project starts with somebody trying to use <EditForm> and <InputText> directly, and ending up with markup that's half styling and half binding logic. Solve this once.
Build a <Field> component that:
- Renders a label, an input, and a validation message.
- Takes
Label,For(the model expression), and a render fragment for the input. - Handles validation styling consistently across the whole app.
The actual input markup goes inside the Field. You'll write five or six variants (text, select, date, checkbox, currency) and reuse them across every form in every project. We've copied roughly the same Field component into a dozen projects now.
This is not framework code. It's project code. Don't put it in a NuGet package and don't generalise it. Keep it 80 lines per project and let it diverge as projects diverge.
SignalR: connection lifecycle is your problem
If you ship a Blazor Server app to production and don't think about reconnection, you will have user complaints within a week. Browsers go to sleep. Wi-Fi drops. People close laptops mid-form.
The default reconnect modal is functional but ugly, and it confuses non-technical users. Replace it. We now style ours as a small unobtrusive toast in the top-right corner that says "Reconnecting…" while it's retrying, and tells the user to refresh if it fails. CSS-only, no JS needed beyond what Blazor already ships.
The second SignalR thing to know: Azure App Service has WebSocket support, but you have to turn it on in the Azure portal. If you forget, your app falls back to long-polling, which works but burns CPU for no reason. This is a five-second config change and it's caught us out three or four times when standing up a new environment.
CSS: scoped, but not religiously
Blazor's CSS isolation (Component.razor.css) is good. We use it for most things. But:
- Layout primitives (cards, fields, buttons, modal scrim) go in a global stylesheet. Otherwise you copy-paste them between components forever.
- Brand tokens (colours, spacing, type) are CSS variables on
:rootin the global sheet. - Component-specific styling goes in the scoped file.
What you avoid: putting everything inline in style="…" attributes. We did this in our first Blazor project and the resulting components were unreadable. Inline styles are fine for one-offs; they are not a CSS strategy.
JS interop: minimal, named, file-per-feature
Every Blazor app needs some JS. Modal scroll lock, focus management, scroll-to-bottom, the odd browser API. Don't fight it.
Our convention:
- One
redowl-interop.jsfile inwwwroot/js(we've tried multiple; one is easier). - All functions hang off
window.redowl. Namespaced, predictable. - Functions are small, dumb, and named for what they do.
redowl.modalOpen,redowl.scrollToBottom. No "framework wrappers" or "interop helpers." - Blazor calls JS via
IJSRuntime.InvokeVoidAsync. JS calls Blazor viaDotNetObjectReferencewith[JSInvokable]on the C# side.
The temptation is always to abstract this. Resist. The minute your interop becomes "clever," it becomes another framework to learn, with worse documentation than the actual frameworks.
What we got wrong
Three things, looking back:
- We over-componentised early. First project had thirty tiny components when ten would have been clearer. A
<Field>is reusable. A<UserNameDisplay>that wraps@User.Nameis not reusable, it's noise. - We adopted MudBlazor / a UI kit too fast. It saved time in the first month and cost time in months six, seven and eight when we needed to customise things the kit didn't expect. Build your own primitives. A button, a card, a modal — none of these are hard, and you control the styling completely.
- We treated
OnInitializedandOnParametersSetas interchangeable. They are not.OnInitializedruns once.OnParametersSetruns every time parameters change. Putting expensive setup in the wrong one causes bugs that look like "the component sometimes doesn't update when I navigate." We now have this as a checklist item in code review.
The patterns that have survived
The shortlist of things that have been in every Blazor project we've shipped since 2022:
StateAwareComponentbase class — subscribes to a scoped state service, callsStateHasChangedon change, unsubscribes on dispose. Twenty lines of code, used everywhere.<Field>component for form inputs.- One global stylesheet + scoped CSS files per component.
window.redowlnamespace for JS interop.- Server rendering with
InteractiveServerrendermode. WASM where it earns its keep, Server everywhere else. - Scoped services with
event Action OnChangefor cross-component state.
None of these are clever. That is the point. Blazor has been stable, productive and predictable because we have not built abstractions on top of it. The framework already gives you a lot; the job is to use it directly and resist the urge to wrap.
Would we still pick it today?
For internal portals and line-of-business systems: without hesitation. For consumer-facing marketing sites: probably not — there are lighter tools for that, though we now build them in Blazor too (this site is one) because the consistency-of-stack outweighs the marginal payload cost. For mobile: that's MAUI, not Blazor — but the model is the same and the same team can build both.
Five years in, Blazor has done what we hoped it would do in 2021: let a small team ship a lot of software, quickly, with high quality, in a stack we can hire for. That's still the bet, and it's still working.
Red Owl IT is a Microsoft software consultancy in Bath. We've shipped 20+ Blazor systems across manufacturing, healthcare, finance, logistics and more. If you're starting (or rescuing) a Blazor project, we'd happily compare notes.