If you want to improve the performance of a Blazor application, you may want to reduce the number of times the UI is recomputed. In Blazor, this means reducing the number of times the StateHasChanged method is called. This method should only be called when the state has changed and the UI needs to be updated. However, there are multiple ways to call it implicitly. One of them is handling an event such as @onclick or @onchange. The method may even be called twice if the handler is asynchronous.
Let's see a demo:
Razor
@page "/"
<p>Clicking on the button triggers a re-render of the component</p>
<button @onclick="OnClick">Refresh UI</button>
<div>Render count: @renderCount</div>
@code{
int renderCount;
void OnClick()
{
}
protected override void OnAfterRender(bool firstRender)
{
renderCount++;
}
}
The default event handler is defined in the ComponentBase class (source):
C#
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
// After each event, we synchronously re-render (unless !ShouldRender())
// This just saves the developer the trouble of putting "StateHasChanged();"
// at the end of every event callback.
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
The two highlighted lines call StateHasChanged even if the event handler does not actually change the state. If your UI is complex, this can be a performance issue. Let's see how to change the default behavior.
The solution to prevent re-rendering the UI after an event is to provide a callback defined in a class that implements IHandleEvent. That way, Blazor will use your implementation of IHandleEvent instead of the default one. Blazor checks if delegate.Target implements the IHandleEvent interface, as you can see in the source code.
Razor
<button @onclick="OnClick">Refresh UI</button>
<button @onclick="SimpleCallback.Create(OnClick)">Do not refresh UI</button>
<div>Click count: @clickCount</div>
<div>Render count: @renderCount</div>
@code {
int clickCount;
int renderCount;
void OnClick()
{
clickCount++;
}
protected override void OnAfterRender(bool firstRender) => renderCount++;
private record SimpleCallback(Action Callback) : IHandleEvent
{
public static Action Create(Action callback) => new SimpleCallback(callback).Invoke;
public static Func<Task> Create(Func<Task> callback) => new SimpleAsyncCallback(callback).Invoke;
public void Invoke() => Callback();
public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => item.InvokeAsync(arg);
}
private record SimpleAsyncCallback(Func<Task> Callback) : IHandleEvent
{
public Task Invoke() => Callback();
public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => item.InvokeAsync(arg);
}
}
Now, the UI is not refreshed when you click on the second button:
#Additional resources
Do you have a question or a suggestion about this post? Contact me!