Prevent refreshing the UI after an event in Blazor

  • Gérald Barré

If you want to improve the performance of a Blazor application, you may want to reduce the number of time the UI is recomputed. In Blazor, this means reducing the number of time the StateHasChanged method is called. This method needs to be called only when the state has changed and the UI needs to be updated. However, there are multiple ways to implicitly call it. One of them is to handle an event such as @onclick or @onchange. The method way even be called twice if the handler is asynchronous.

Let's see a demo:

@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):

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 2 highlighted lines calls StateHasChanged even if the event handler doesn't actually change the state. If your UI is complex, this may be a performance issue. Let's see how to change the default behavior.

The solution is prevent re-rendering the UI after an event is to provide a callback defined in a class that implement IHandleEvent. That way Blazor will call your implementation of IHandleEvent instead of the default one. Indeed, Blazor check if delegate.Target implement the IHandleEvent interface as you can see in the source code.

<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!

Follow me:
Enjoy this blog?Buy Me A Coffee