Is your Blazor app slow? 7 causes and how to fix them
Your Blazor app lags on every click, a grid freezes, or server memory keeps growing? The seven most common causes, with before and after code, for .NET 10.
Vlado Pandžić · Founder · Senior .NET architect
Published · 4 min read
At the start, a Blazor application flies. A few screens, little data, everything responds instantly. A year later a button click lags, the order grid freezes for a couple of seconds, and the server uses more and more memory as the day goes on.
Blazor is rarely the culprit on its own. It is almost always one of these seven causes, and all of them can be fixed without rewriting the application. Each one is here with code, for .NET 10.
First: measure what renders
The most common surprise is how many times a component renders. A temporary log shows it in a minute:
protected override void OnAfterRender(bool firstRender)
=> Logger.LogDebug("{Component} rendered", GetType().Name); // temporary, only while measuring
If one click puts fifty lines in the log, you have found problem number one.
1. Everything re-renders on every click
Blazor re-renders a child when its parameters change. But when a parameter is a complex object, such as a whole order, Blazor can’t tell whether something inside it changed, so it re-renders every time. Simple types such as int and string don’t re-render the component until their value changes.
@* before: the whole object, so the row re-renders on every parent change *@
<OrderRow Order="order" />
@* after: only what the row displays *@
<OrderRow Number="@order.Number" Customer="@order.CustomerName" Total="@order.Total" />
When that isn’t possible, the component can decide for itself whether to render:
private int lastVersion;
protected override bool ShouldRender()
{
var changed = Order.Version != lastVersion; // render only when the order really changed
lastVersion = Order.Version;
return changed;
}
2. Lists without @key
When a list changes, Blazor without @key compares rows by position. One row inserted at the top means every row below it re-renders, and state inside the rows can end up in the wrong row.
@foreach (var order in orders)
{
<OrderRow @key="order.Id" Number="@order.Number" Total="@order.Total" />
}
3. Grids with thousands of rows
Five thousand rows rendered at once means five thousand components in memory and on screen, although the user sees twenty of them. Virtualize renders only what is visible:
<Virtualize Items="orders" Context="order">
<OrderRow @key="order.Id" Number="@order.Number" Total="@order.Total" />
</Virtualize>
QuickGrid does the same with Virtualize="true", and for really large data both can fetch rows from the server piece by piece.
4. Data is loaded twice
With prerendering, OnInitializedAsync runs twice: once on the server for the first view, and once more when the component becomes interactive. That means two identical database queries and a flicker on screen. In .NET 10, one attribute fixes it:
@code {
[PersistentState]
public List<OrderSummary>? Orders { get; set; }
protected override async Task OnInitializedAsync()
{
Orders ??= await OrderService.GetOpenAsync(); // the second time, the data is already there
}
}
5. StateHasChanged too often
A component that follows prices, statuses or messages in real time easily ends up with dozens of renders a second. The user can’t tell 4 refreshes a second from 40, but the server and the browser certainly can.
protected override void OnInitialized()
{
Prices.Changed += OnPriceChanged;
renderTimer = new Timer(_ =>
{
if (!dirty) return;
dirty = false;
InvokeAsync(StateHasChanged); // at most four renders a second
}, null, 250, 250);
}
private void OnPriceChanged(object? sender, PriceChangedEventArgs e) => dirty = true; // just note the change
6. Server memory that only grows
A component that subscribes to an event or starts a timer and never cancels it never dies. On Blazor Server that means memory grows with every user and every open screen, until the next restart.
@implements IDisposable
@code {
public void Dispose()
{
Prices.Changed -= OnPriceChanged; // without this, the component stays in memory
renderTimer?.Dispose();
}
}
7. The wrong render mode for the screen
In .NET 10 every page can have its own render mode. Interactive Server is great for internal screens on a good network, but on a poor connection every click waits for the network. Pages that are only read don’t need interactivity at all.
@* MonthlyReport.razor: read only, static rendering with no connection to the server *@
@page "/reports/monthly"
@* Orders.razor: an interactive screen *@
@page "/orders"
@rendermode InteractiveServer
Which mode for which screen is in the table on the Blazor specialisation page.
In which order
- MeasureHow often what renders
- SplitSmaller components, simple parameters
- VirtualizeBig lists and grids
- Clean upDispose and the render mode
Often a slow Blazor screen isn’t a Blazor problem at all, but a problem with the queries behind it. If a screen takes long to load but renders little, see the article on EF Core performance.
How we work
ProCoding is a .NET studio from Split, Croatia, and we have been shipping Blazor to production since 2020, from software used to build electric cars to battery storage control. A Blazor health check finds which of these causes is slowing your application down and gives you a prioritised list of fixes. The first step is a free 30-minute call.
Sources
- ASP.NET Core Blazor performance best practices, Microsoft Learn
- Blazor rendering performance, Microsoft Learn
- Blazor component virtualization, Microsoft Learn
- Prerendered state persistence, Microsoft Learn
- Component disposal, Microsoft Learn
This article is general information only, not legal, tax, financial or other professional advice. Scenarios, examples and calculations are illustrative. Terms of use and disclaimer.