Adopting Blazor best practices is the only way to get your ASP.NET Core applications on the right track. Rather than juggling between C# for the backend logics and JavaScript for the frontend interactions, you can use Blazor to build interactive web apps using only C#. It makes your development process easier, reduces context switching, and lets you create a more cohesive codebase.
That said, working with Blazor can sometimes throw you a curveball. If you’re building a large-scale app, you may run into slow load times and clunky rendering. You may also struggle with state management, especially when trying to keep data in sync across multiple components. Plus, connecting a server-side Blazor app to a database can be a nightmare without a solid data access layer — and if you are using Blazor WebAssembly, things get worse.
In this article, we’ll help you navigate these common pitfalls. We’ll share some key Blazor best practices to optimize your architecture and enhance performance, ensuring your applications are efficient and user-friendly.
Table of contents
- ASP.NET Core Blazor architecture best practices
- Performance optimization best practices
- Reliability and security best practices
- Benefits of using ASP.NET Core Blazor and dotConnect
- Conclusion
ASP.NET Core Blazor architecture best practices
Building complex applications is never easy. However, implementing Blazor architecture best practices when designing component interactions and managing state can make quite a difference. It all comes down to choosing the right architecture for your project, keeping your components modular and reusable, and leveraging the power of C# and .NET.
Choose the right hosting model
Depending on your app’s requirements and personal preference, you have two primary options: Blazor WebAssembly (WASM) or Blazor Server.
Here’s how they stack up against each other:
Feature | Blazor WebAssembly | Blazor Server | Architecture | Features a decoupled architecture that requires a Web API backend. | Connects directly to the database using dependency injection. |
---|---|---|
Frontend Flexibility | API operates independently; easy to switch frontends (e.g., React, Vue.js). | Tightly coupled with server-side processing. |
Client-Side Processing | Runs C# code directly in the browser. | All processing happens on the server. |
Performance | Since it executes right in the browser, near-native performance. | Performance can suffer from latency because every action involves a round trip to the server. |
User Experience | Responsive; suitable for users with slow or intermittent connections. | May not provide fast response times for public websites. |
Real-Time Communication | Not inherently supported; can be implemented using SignalR with additional setup. | Built-in support via SignalR. |
Offline Capabilities | Can work offline after the initial load. | Doesn’t support offline functionalities. |
Security | Code runs in the client browser, which may expose sensitive logic to users. | Server-side execution secures critical data from exposure. |
Use Cases | Ideal for single-page applications, progressive web apps, and offline apps (e.g., inventory management, CRM tools). | Best for enterprise applications in finance, healthcare, and government services. |
Scalability | Limited by client resources; relies on the user’s device capabilities. | Scales well with server resources; can handle many concurrent users. |
Not sure which model to use? Well, you can have it both ways with ASP.NET Core Blazor Hybrid. It combines client-side and server-side functionalities. At the same time, you can manage high-security operations, like sensitive data transactions, through a WebSocket connection to your backend API.
Build efficient and streamlined components
Blazor’s component-based architecture is all about creating small, focused components that encapsulate both UI and logic. This minimizes complexity and makes it easier to scale your project as it grows.
Here are a few tips to work with Blazor components:
- Keep presentation components simple. They should only display the information they’re given and trigger events when needed. For tasks like checking if a username is already taken, it’s okay to use injected services. However, avoid using services to fetch data directly within your presentation components.
- Centralize your business logic. Keep business functions within services that interact with the back end. Your presentation and container components should only rely on these services to access and manipulate data.
- Favor composition over inheritance. This means building components from smaller, reusable pieces rather than creating complex hierarchies — which makes your code flexible and easier to manage.
- Use container components for coordination. Implement container components (like pages) to coordinate multiple presentation components. They handle the heavy lifting of fetching data from services and then distribute it to the presentation components.
- Organize your project structure. Group shared components into a Shared project with clear folders. For larger features, create separate folders or even separate projects for components and services that are common across multiple pages within that feature.
Maximize C# and .NET integration
Whether you’re building a new web app from scratch or connecting it to existing services, use C#’s asynchronous programming, LINQ, and generics to write concise, efficient, and type-safe code. Another good practice is to write your business logic once in C# and reuse it in both your client-side and server-side projects.
For example, if you have a utility class that performs calculations or data transformations, you can reference it in both your Blazor WAMS and Blazor Server apps. This way, you will prevent duplication and maintain a consistent codebase.
In addition, since Blazor is built on ASP.NET, you’re free to reach for a ton of pre-built tools that save you time and effort in the long run. Use .NET’s libraries like Newtonsoft.Json for JSON serialization, NLog and Serilog for logging, and IdentityServer for authentication. Also, focus on building reusable UI components using Razor syntax. It allows you to create a consistent look and feel across your application, which simplifies updates and maintenance.
For database interactions, use Entity Framework Core. This ORM abstracts the complex SQL queries behind the scenes, and you don’t have to write them yourself. You do the CRUD operations directly in your C# code, which makes working with your database a whole lot easier and keeps things type-safe.
Interested in how to efficiently connect different databases with a Blazor app? Check out these tutorials:
How to use SQLite and Entity Framework Core in Blazor >
Explore how to use the code-first approach to integrate SQLite with a Blazor application to design and manage a database directly from the C# code
How to use MySQL and Entity Framework Core in Blazor >
Learn the Code-First method to connect MySQL to a Blazor application, giving you full control over database design and management with C# code.
How to use PostgreSQL and Entity Framework Core in Blazor >
Discover the best practices to integrate PostgreSQL with your Blazor application, enabling database design and management directly from C# code.
Performance optimization best practices
.NET developers have been talking about Blazor’s performance for a while now, with some expressing frustrations about slow load times and overall sluggishness. While there’s a bit of truth in this, with a proper approach, you can have pretty fast and responsive apps that meet your users’ expectations.
Master сomponent life cycle management
Blazor’s component lifecycle is similar to other frameworks, like React and Angular. You will be able to hook into each stage of the process with methods like “OnInitialized”, “OnParametersSet”, and “OnAfterRender” to add your custom behavior.
The more calls to these lifecycle methods Blazor has to make, the greater the overhead and impact on performance. So, the key lies in building components that are lightweight and optimized.
How? Avoid creating thousands of component instances for repetitive UI elements. When it makes sense, inline child components in their parent components rather than making an entire separate tree for them. For example, instead of having a separate “Notification” component for each alert in a list, simply render them directly within a loop in the parent component “NotificationList.”
Optimize rendering speed
First, focus on reducing auto or unnecessary re-renders. You should always set the parameters of child components using immutable types like “string”, “float”, and “bool”. This will allow Blazor to skip the re-rendering if values have not changed. In the case of complex parameters, you need to override the “ShouldRender” method to control when a component needs to re-render.
For example, if you are working with an app that contains a component showing a grid of products, make sure to pass as a parameter one immutable list of product IDs. Something like: “new List<int> { 101, 102, 103 }”. Blazor won’t waste time re-rendering the UI until you add new products or remove existing ones.
Try also the agnostic render mode in .NET 8. This lets you decide whether to use server-side or client-side rendering, depending on what works best for your application.
So, let’s say you have a “UserProfile” component displaying information about users. You can start with server-side rendering just to get that quick initial load. Then, you can switch to client-side rendering for faster updates when users edit their profiles. This way, they won’t have to wait around and everything will feel much smoother.
Implement efficient event handling
When an event is triggered, such as a button, it can cause the component to re-render. If your event handlers are designed efficiently, you can minimize unnecessary renders and keep your UI snappy even when there’s a lot going on.
All you need to do is use the “IHandleEvent” interface. This will let you handle events without forcing “StateHasChanged()” after each event — which is what typically happens when you handle events directly in your component.
Here’s an example:
TaskList.razor
@page "/tasklist"
@implements IHandleEvent
<h3>Task List</h3>
<input @bind="newTask" placeholder="Add new task" />
<button @onclick="AddTask">Add Task</button>
<ul>
@foreach (var task in tasks)
{
<li>
<input type="checkbox" @onclick="() => ToggleTaskCompletion(task)" checked="@task.IsCompleted" />
@task.Name
</li>
}
</ul>
@code {
private List<TaskItem> tasks = new();
private string newTask;
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg) =>
callback.InvokeAsync(arg);
}
In this example, if you use lambda expressions for each checkbox’s onClick event, Blazor might get a bit sluggish when rendering a large number of tasks. To speed things up, create a collection of task objects. Assign each task’s “@onclick” delegate to an Action, which prevents Blazor from rebuilding all the task delegates on each render.
The result? A noticeable performance boost, especially when dealing with many interactive elements.
Use virtualization for large data sets
If your applications load a lot at once, your users are in for quite some time before they are able to use it. Now, if we are talking about thousands or even more entries, what do you think would happen? The wait time will become unbearable.
An easy way to avoid this is using the “Virtualize<TItem>” component. It will render only the elements currently in view, improving responsiveness and reducing the load on the UI.
Take a look below to see an example of how you can implement it:
Orders.razor
@page "/orders"
<h3>Order List</h3>
<ul>
<Virtualize Items="orders" Context="order">
<li>
<div>@order.Id</div>
<div>@order.Description</div>
</li>
</Virtualize>
</ul>
Lazy load your components
Much like virtualization, lazy loading means fetching only the necessary data or components your users need at any given moment. However, lazy loading specifically focuses on delaying the loading of resources until they’re actually needed, which can enhance initial load times even more.
The simplest way to do this is to use the @page directive with a RouteView component to load specific components based on the current route.
Following the example above, you can set up your routing to load the “OrderList” component when navigating to the “/orders” route like this:
Orders.razor
@page "/orders"
<h3>Order List</h3>
<RouteView RouteData="@RouteData" DefaultPage="/@typeof(OrderList)" />
OrderList.razor
@page "/orders/list"
<h3>Order List</h3>
<ul>
<Virtualize Items="orders" Context="order">
<li>
<div>@order.Id</div>
<div>@order.Description</div>
</li>
</Virtualize>
</ul>
Optimize JavaScript interop
Communication between .NET and JavaScript can be a bit slow because it involves a few extra steps. First, calls are asynchronous, each call can introduce latency. Second, data needs to be translated to a format that both can understand (JSON), which takes some time.
A few things you can do to improve JavaScript Interop’s speed are:
- Reduce the number of calls. Instead of calling a JavaScript function separately for each item in a list, pass the entire list as a single parameter and handle it in one call.
- Use the “[JSImport]” and “[JSExport]” attributes. This lets .NET code and JavaScript talk directly without needing to go through the traditional interop API, which can be slower and less stable.
- Cache results. If a JavaScript function returns results that don’t change frequently, consider caching those results to avoid unnecessary calls to JavaScript.
- Try synchronous calls. This is only available in Blazor WASM apps (ASP.NET Core 5.0 or later), but it’s a great way to get instant replies from JavaScript. However, use it sparingly. It can slow things down if used too much. For immediate answers from JavaScript, use “DotNet.invokeMethodAsync” in your .NET code. To call a .NET method from JavaScript, use “DotNet.invokeMethod”.
Minimize app size with AOT compilation and IL trimming
In Blazor development, size matters. A large app takes longer to download, making users wait and potentially abandon your app before it even loads. This is especially true for Blazor WebAssembly (WASM) applications, as the entire app and its dependencies need to be downloaded to the client’s browser.
To optimize your app’s size, you can use two main techniques: Ahead-of-time (AOT) compilation and Intermediate Language (IL) trimming.
With traditional compilation, you build all the pieces separately and assemble them on-site (the browser). AOT compilation, on the other hand, pre-assembles everything into a single, ready-to-run package.
This pre-assembly makes your app run faster, especially for tasks that require a lot of processing power. However, the downside is that the final package is larger and takes longer to download. That’s why you should couple AOT compilation with runtime relinking, which removes unused parts of your app at runtime.
In contrast, IL trimming analyzes your app’s code and removes any redundant parts included in the final build.
Reliability and security best practices
Keeping users happy means delivering a reliable and safe experience. But security is also crucial to protect the integrity of your application, especially if you’re working on internal enterprise apps that handle sensitive data.
Setup proper error handling and logging
If your Blazor application throws an exception and doesn’t have proper error handling in place, it will throw a very cryptic error message that will leave users confused and frustrated. Not exactly the experience you’re trying to give them, right?
To prevent this meltdown, implement a solid error-handling solution that will display friendly messages while logging the actual errors for your review. This way, you can diagnose issues without exposing sensitive information.
You can use the built-in ASP.NET Core logging framework, which lets you record information, warnings, and errors in different places like consoles and files. You can also integrate with third-party libraries like Serilog or NLog. Serilog gives you structured logging, so analysing complex data is much easier. NLog makes it simple to route your logs out to various targets.
When logging, always be extra careful about sensitive data. Log messages should not directly expose personal data; instead, use placeholders. For example, log “User {username} attempted to log in” instead of including details which may be too sensitive.
Enforce ASP.NET Core authentication and authorization
Use ASP.NET Core Identity to manage user accounts and roles smoothly. With the “[Authorize]” attribute, you can restrict access to specific controllers or Blazor pages based on user roles (like Admin or User).
To get started, you’ll need to configure authentication within your Startup.cs file. This involves adding services such as “AddAuthentication” and “AddAuthorization” to define the access policies that dictate who can access what based on roles or claims.
For example, if you want to ensure that only users with an Admin role can access certain Blazor pages, you can create a policy for that and apply it using the “[Authorize(Policy = “RequireAdminRole”)]” attribute on your Blazor components.
Note: ASP.NET Core supports both role-based and claims-based authorization models. In a nutshell, role-based is meant to control access based on user roles. In contrast, claims-based offers a lot more detail in what you can evaluate against per-user attributes. This gives you the opportunity to apply complex authorization rules tailored to your application’s needs, enhancing security and adaptability as your user base grows.
Stick to HTTPS
If your Blazor ASP.NET app handles user data, HTTPS is non-negotiable. It encrypts everything transmitted between the client and server, which helps prevent eavesdropping and man-in-the-middle attacks.
Head over to your “Startup.cs” file and make sure you include the “UseHttpsRedirection middleware”. This simple step redirects all HTTP requests to HTTPS.
Tackle OWASP security risks
Following the above Blazor best practices can help you fight off some of the top 10 OWASP risks, like broken access control and cryptographic failures. However, you still need to keep an eye out for several other potential threats.
Here are some additional OWASP issues to watch out for, along with how to address them:
- Server-side request forgery (SSRF). Validate and sanitize all user-supplied URLs to restrict requests to trusted domains only.
- Security misconfiguration. Regularly review and harden server configurations. It’s a good idea to use automated tools like Azure Security Center for ongoing security assessments.
- Unrestricted resource consumption. Use rate limiting and request throttling to control API usage and prevent denial-of-service attacks.
- Cross-site scripting (XSS) injection. Validate user input and properly encode output. This will stop malicious scripts from running in the browser.
Benefits of using ASP.NET Core Blazor and dotConnect
Managing data connections and ensuring smooth data flow in your Blazor apps can be a real headache. You might face complex data access patterns, performance bottlenecks, or compatibility issues with different data sources.
dotConnect helps streamline these challenges. Built over the ADO.NET architecture, its data providers work with ASP.NET Core Blazor and other .NET frameworks like ASP.NET MVC and .NET Core.
- Fast data access. Experience super-fast component loading and optimized data handling.
- Direct connectivity. Connect directly to most major databases and data sources using Entity Framework or Entity Framework Core.
- Enhanced performance. Benefit from batch updates and LINQ support to improve performance and reduce database round trips.
- Visual data modeling. Quickly build and manage your data models visually with dotConnect’s Entity Developer.
Conclusion
Implementing Blazor best practices goes a long way in enhancing your application’s responsiveness, but having a robust connectivity solution is essential for optimizing data flow and component rendering. Try dotConnect for fast, efficient data handling and loading. It seamlessly integrates across all ASP.NET Core Blazor apps, optimizing your performance even if you decide to branch out into other .NET frameworks.