This package gives you one simple API for browser web workers in Blazor.
Use it when you want to:
- move CPU-heavy work off the UI thread
- keep the app responsive while work is running
- run custom JavaScript worker jobs
- run exported C# methods in JS
- monitor progress, cancel requests, and inspect worker pool state
dotnet add package Soenneker.Blazor.WebWorkersSoenneker.Blazor.WebWorkers manages worker pools for you.
In most apps, the flow is:
- Register
IWebWorkersUtil - Call
Initialize() - Create a worker pool
- Queue work
- Optionally monitor progress, cancel work, or inspect snapshots
The same IWebWorkersUtil service works for both:
- JavaScript workers
.NETworkers
Register the service in Program.cs:
builder.Services.AddWebWorkersUtilAsScoped();Inject it where you want to use it:
@inject IWebWorkersUtil WebWorkersInitialize it once before first use:
await WebWorkers.Initialize();Point the pool at your worker script:
await WebWorkers.CreatePool(new WebWorkerPoolOptions
{
WorkerCount = 4,
ScriptPath = "js/workers/app.worker.js"
});That creates the default JavaScript pool.
Pass a workload name and payload:
using System.Text.Json;
using Soenneker.Blazor.WebWorkers.Dtos;
WebWorkerResult<JsonElement> result = await WebWorkers.Run<JsonElement>(
"prime-analysis",
new
{
upperBound = 180000
},
progress =>
{
Console.WriteLine($"{progress.Percent:0}% - {progress.Message}");
return ValueTask.CompletedTask;
});Your worker script is responsible for understanding the workload name and payload.
await WebWorkers.CancelRequest("default", jobId);
WebWorkerCoordinatorSnapshot snapshot = await WebWorkers.GetCoordinatorSnapshot();This is the best fit when your worker logic already lives in JavaScript, or when you want full control over the worker script.
Important points:
- JavaScript pools use
WebWorkerBackend.JavaScript - the default pool name is
"default" - you usually only need
WorkerCountandScriptPath - jobs are queued with a
workloadNameand optionalpayload - you can report progress back while work is running
You can also target a specific named pool:
await WebWorkers.CreatePool(new WebWorkerPoolOptions
{
Name = "images",
WorkerCount = 2,
ScriptPath = "js/workers/image.worker.js"
});
WebWorkerResult<JsonElement> result = await WebWorkers.Run<JsonElement>(
"images",
"generate-thumbnail",
new
{
width = 300,
height = 300
});If a worker file ships from an RCL, build the static asset path like this:
string workerPath = WebWorkerAssetPaths.WorkerFromPackage(
"Soenneker.Blazor.Opfs",
"opfs.worker.js");Then use that path as the pool's ScriptPath.
This package can also run exported C# methods inside a browser worker by booting a second .NET WebAssembly runtime in that worker.
This path is useful when:
- your work is already written in C#
- you want to keep background logic in your Blazor app
- you do not want to hand-write a JavaScript worker for that job
The .NET worker path requires:
- Blazor WebAssembly
AllowUnsafeBlocks=truein the app.csproj- exported worker methods defined in the main app assembly
- worker methods marked with
[JSExport]
Example .csproj setting:
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using Soenneker.Utils.Json;
[SupportedOSPlatform("browser")]
public static partial class WorkerExports
{
[JSExport]
public static string? AnalyzePrimeRange(int upperBound)
{
return JsonUtil.Serialize(new
{
upperBound,
message = "Ran inside a .NET worker."
});
}
}using Soenneker.Blazor.WebWorkers.Enums;
using Soenneker.Blazor.WebWorkers.Options;
await WebWorkers.CreatePool(new WebWorkerPoolOptions
{
Backend = WebWorkerBackend.DotNet,
WorkerCount = 1
});You can call it with a request object:
using Soenneker.Blazor.WebWorkers.Dtos;
using Soenneker.Blazor.WebWorkers.Enums;
WebWorkerResult<string?> result = await WebWorkers.Run<string?>(new WebWorkerRequest
{
Backend = WebWorkerBackend.DotNet,
MethodName = "MyApp.WorkerExports.AnalyzePrimeRange",
Arguments = [220000]
});Or use the expression-based overload:
WebWorkerResult<MyResult> result =
await WebWorkers.Run(() => WorkerExports.RunAnalysisAsync(220000));The expression-based overload is often the easiest option because it avoids building the request manually.
The main service you work with. It handles:
- initialization
- pool creation and destruction
- job execution
- cancellation
- pool and coordinator snapshots
Used when creating a pool.
The most important properties are:
BackendNameScriptPathWorkerCountWorkerTypeRuntimeScriptPathBootConfigPathRestartFaultedWorkers
Used when you need full control over a queued request.
The most important properties are:
PoolNameBackendRequestIdWorkloadNameMethodNamePayloadArgumentsTimeoutMs
You can:
- receive progress callbacks while a job is running
- cancel a queued or running request
- inspect one pool or all pools
- inspect a full coordinator snapshot
Examples:
await WebWorkers.CancelRequest("default", requestId);
WebWorkerPoolSnapshot? pool = await WebWorkers.GetPoolSnapshot("default");
IReadOnlyList<WebWorkerPoolSnapshot> pools = await WebWorkers.GetPoolSnapshots();
WebWorkerCoordinatorSnapshot snapshot = await WebWorkers.GetCoordinatorSnapshot();- JavaScript and
.NETworkers share the same top-level service:IWebWorkersUtil - JavaScript jobs use
WorkloadNameplusPayload .NETjobs useMethodNameplusArguments- the
.NETworker path is for Blazor WebAssembly .NETworker methods should usually return simple values or serialized JSON- if your worker code naturally returns
ValueTask, expose a smallTask-returning[JSExport]wrapper - cancellation of a running
.NETworker request may require terminating and replacing the backing worker
Use the JavaScript path when:
- you already have worker logic in JavaScript
- you need a custom browser worker script
- your workload is naturally message-based
Use the .NET path when:
- your workload is already implemented in C#
- you want to stay in C# as much as possible
- you are building a Blazor WebAssembly app (Not available in Server)
