Blazor Server Best Practices - Part 1
After exploring Blazor Server and utilizing it in a production environment, I noticed a significant lack of comprehensive best practices available online. Motivated to address this gap, I have taken it upon myself to craft and document my personal set of best practices for Blazor Server.
In this blog post, I am excited to share these practices with you.
Estimate the Memory Usage
When it comes to determining the required amount of memory for your circuits, there are a few factors to consider. The memory consumption for circuits can be calculated using the following formula:
(Active Circuits × Per-circuit Memory) + (Disconnected Circuits × Per-circuit Memory)
As a starting point, each circuit typically utilizes around 250 KB to 300 KB of memory, depending on the specific application and how states are managed. However, keep in mind that this is just a general baseline and can vary based on your specific requirements.
In the process of configuring your circuits, you have the ability to specify two important options:
DisconnectedCircuitMaxRetained and DisconnectedCircuitRetentionPeriod.
The first option, DisconnectedCircuitMaxRetained, allows you to define the maximum number of disconnected circuits that the server can retain. This parameter influences how much memory is allocated for disconnected circuits. The second option, DisconnectedCircuitRetentionPeriod, allows you to set the maximum amount of time that a disconnected circuit will be held in memory before being torn down. This setting can also affect the overall memory consumption for disconnected circuits.
It is essential to accurately estimate the amount of memory required for your workload.
By measuring and estimating how much memory you need, you can utilizes Azure Web Apps to host and serve your Blazor Server application in most cases with a peace of mind, and you do not need to utilize Azure SignalR Service to manage circuits.
You should consider using Azure SignalR only when dealing with extreme load scenarios.
Find more information about circuit handler options here.
Deploy to Azure Web Apps with WebSocket enabled
When you are not utilizing Azure SignalR Service for circuit management and use Azure Web Apps only to host and serve your application instead, it is crucial to enable WebSockets to achieve optimal results.
WebSockets provide a real-time connection between the client and server, allowing for efficient and reliable communication. By enabling WebSockets in Azure Web Apps, you can leverage their benefits and eliminate the need for HTTP Long Polling.
When WebSockets are disabled, Azure App Service simulates a real-time connection using HTTP Long Polling. However, it is important to note that HTTP Long Polling is noticeably slower compared to running the application with WebSockets enabled. This is because HTTP Long Polling relies on constantly polling the server for updates, resulting in higher latency and increased resource consumption.
To ensure a smoother and more efficient client-server connection, it is recommended to enable WebSockets in Azure Web Apps. By doing so, you eliminate the delay caused by constant polling and allow for a true real-time experience for your application's users.
In summary, when not utilizing Azure SignalR Service for circuit management and solely relying on Azure Web Apps to host and serve your application, enabling WebSockets becomes essential.
Use DbContextFactory with Entity Framework Core
Blazor Server is a stateful application framework, and the user's state is held in the server's memory in a circuit, which creates a unique challenge for the Entity Framework.
By default, ASP.NET Core registers the DbContext as a scoped service; however, this can be problematic because the instance is shared across components within the user's circuit.
The recommended pattern for using the Entity Framework in a Blazor Server application is to register the DbContextFactory and then use it to create a new instance of the DbContext for each operation. By default, the DbContextFactory is registered as a singleton service, so only one copy of the factory exists for all users of the application.
You should use one context per operation.
You can use the factory to create a context that exists for the lifetime of a component.
Check Microsoft recommendations on this topic here.
Implement a Global Exception Handler
When developing Blazor applications, it is crucial for developers to have a clear understanding of how the framework deals with exceptions. This knowledge enables them to take appropriate steps to ensure maximum reliability, as well as effectively detect and diagnose errors.
Blazor Server, being a stateful framework, operates on the premise that users maintain a connection known as a circuit to the server while interacting with the application. It is important to note that Blazor treats most unhandled exceptions as critical to the stability of that circuit.
If an unhandled exception occurs, it is considered fatal to the circuit.
As a result, the user's ability to continue using the application is compromised. In such cases, the only recourse for the user is to reload the page, creating a new circuit and effectively starting the application from scratch and you do not want it as much as possible. To maximize reliability and handle exceptions effectively in Blazor applications, it is a best practice to implement a global exception handler.
Microsoft has recommended an approach for Blazor Server here. The idea is very simple, but effective. You should implement an Error Handler component and pass it down to your child component as a cascading value. Following this approach you can fully customize how you want to deal with exceptions from both UI and Exception Logging perspectives.
Understand the places where errors may occur and how you want to deal with them.
Do Not Start with Designing Reusable Component
I LOVE to build everything reusable - if I had a components library company!!
Building reusable component comes with a price tag. And this is why you need to understand when is appropriate to refactor markup into a reusable component.
Don’t build reusable components unless:
You have a strong reason about why something has to be re-usable.
From the design, it is clear to you that building a reusable component ‘saves you time’ and ‘make the UI/UX consistent’
The component supposed not to have too many flavors and options
Do not wrap (and proxy) a 3rd party UI component into your ‘own’ component with the promise of flexibility to change the underlying component later down the track.
Reflect Disconnect and Reconnect on the UI
Default user interface and the whole user experience for circuit disconnect and reconnect sucks.
How to improve the user experience?
Adjust the re-connection retry count and interval by specifying maxRetries and retryIntervalMilliseconds on reconnectionOptions.
Use reconnectionHandler to let user if the connection has dropped.
Configure SignalR timeouts and Keep-Alive
Use CreateInboundActivityHandler method on CircuitHandler to monitor circuit activity. Use reconnectionHandler to perform logging from the client side when a circuit is connected (after reconnect).