Blazor WebAssembly is single-threaded and executes everything on the UI thread. This means a long-running operation can freeze the UI. In a classic application, you would start a new thread to run the work in the background. However, this is not possible with WebAssembly, as threads are not yet part of 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 thread support, you must explicitly yield control to the UI at regular intervals so it can update. This is similar to Application.DoEvents in Windows Forms, but Blazor has no equivalent method. Instead, you can use Task.Yield()/Task.Delay(1) to let other tasks run, keeping the UI responsive. It is not ideal, but it is the only option without thread support.
Note that Task.Yield() does not always work. If the UI still freezes, use Task.Delay(1) to ensure the browser has time to render.
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, the .NET runtime used by WebAssembly, has merged a pull request to support threads. This means you may be able to remove this workaround in a future version of Blazor. There is also an open issue tracking support for running Blazor on a worker thread.
#Additional resources
Do you have a question or a suggestion about this post? Contact me!