How to prevent the UI from freezing while executing CPU intensive work in Blazor WebAssembly

 
 
  • Gérald Barré

Blazor WebAssembly is currently single-threaded and executes everything on the "UI thread". This means that a long operation can freeze the UI. On a classic application, you would start a new thread to do this job in the background. But this is not something you can do using WebAssembly. Indeed, threads are not yet available in WebAssembly (at least not in the WebAssembly specification)

If you try to start a new thread, you'll get an exception:

blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Cannot start threads on this runtime.
System.NotSupportedException: Cannot start threads on this runtime.
  at (wrapper managed-to-native) System.Threading.Thread.Thread_internal(System.Threading.Thread,System.MulticastDelegate)
  at System.Threading.Thread.StartInternal (System.Object principal, System.Threading.StackCrawlMark& stackMark) <0x2e50290 + 0x00008> in <filename unknown>:0
  at System.Threading.Thread.Start (System.Threading.StackCrawlMark& stackMark) <0x2e50150 + 0x0004e> in <filename unknown>:0
  at System.Threading.Thread.Start () <0x2e50010 + 0x0000e> in <filename unknown>:0
  at MyBlazorApp.Pages.Index.OnClick () [0x000a7] in C:\Users\meziantou\source\repos\BlazorApp5\BlazorApp5\Pages\Index.razor:22
  at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion (System.Threading.Tasks.Task task) <0x2e4adf8 + 0x000da> in <filename unknown>:0
  at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask (System.Threading.Tasks.Task taskToHandle) <0x2e4d498 + 0x000b6> in <filename unknown>:0

Without support for threads, you need to explicitly give time to the UI to update itself at regular intervals. It is very similar to Application.DoEvents in Windows Forms. In Blazor there is no similar method. However, you can use Task.Yield()/Task.Delay(1) to give time to other tasks to execute, so the UI can stay responsive. This is far from being nice but this is the only way to do it in the absence of threads.

Note that sometimes Task.Yield() doesn't work. In this case, you can try Task.Delay(1) to be sure it gives time to the browser to do the rendering.

Razor
<div>@i</div>
<button @onclick="DoLongJob" disabled="@isDisabled">Process</button>

@code {
    int i = 0;
    bool isDisabled;

    async Task DoLongJob()
    {
        isDisabled = true;
        await Task.Yield(); // Ensure the button is updated (disable)

        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        while (stopwatch.Elapsed < TimeSpan.FromSeconds(4)) // Run for 4 seconds
        {
            i++;

            // Update the UI every 100 iterations
            if(i % 100 == 0)
            {
                StateHasChanged();
                await Task.Delay(1);
            }
        }

        isDisabled = false;
    }
}

Mono, which is the .NET runtime for WebAssembly, has merged a Pull Request to support threads. This means that maybe you'll be able to remove this ugly code in a future version of Blazor. Also, there is an issue for running Blazor into a worker thread.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub