{"slug": "running-background-tasks-in-blazor-with-web-workers-exploring-the-net-11-preview", "title": "Running background tasks in Blazor with Web Workers: Exploring the .NET 11 preview - Part 1", "summary": "The article explores the new Web Worker project template introduced in the .NET 11 preview SDK, which allows Blazor WebAssembly applications to run CPU-intensive tasks on background threads without blocking the UI. It explains that Web Workers provide a way to achieve multi-threading in the browser's single-threaded JavaScript environment by enabling communication between background threads and the main UI thread through message passing. The post demonstrates how to add this template to an existing Blazor app, specifically showing how to replace static JSON data loading with a Web Worker that generates weather forecast data.", "body_md": "In this post I take a look at the new Web Worker template available in the .NET 11 SDK, how to add it to an existing app, what the code is doing behind the scenes, and how to use it to run CPU intensive work without blocking the UI.\n\nThis post was written using the features available in .NET 11 preview 3. Many things may change between now and the final release of .NET 11.\n\n[Why do you need Web Workers?](#why-do-you-need-web-workers-)\n\nOne of the neat things about .NET running in the browser using Blazor is that you can easily handle all sorts of complex domains such as image processing, document parsing, or data manipulation. This is simple enough when you are running using Blazor Server, and you can run these CPU intensive tasks on the server. Unfortunately, if you are using Blazor WASM, it's not always so simple.\n\nThe core problem is that the JavaScript engine (e.g. [V8](https://v8.dev/)) is fundamentally single-threaded. That means if you're doing CPU-intensive work, then nothing else can happen—most importantly, the UI will become unresponsive, and the browser may even suggest closing the page. A related issue is that you can't use multi-threading (because there's only a single-threaded event loop).\n\nThat's where [ Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) come in. Web workers are a way to get multi-threading back, by running JavaScript (or WebAssembly) on a background thread. They can then communicate back to the main UI thread by posting messages to it.\n\nIf you're used to creating .NET Windows Forms applications, this communication is somewhat analogous to the\n\n[single-threaded-apartment model]that requires you only modify UI elements on the STA Thread.\n\nWeb Workers give you \"true\" multi-threading, but it is a somewhat limited version. It is a much more \"cooperative\" form of multi-tasking than you might be used to in .NET (where using `async`\n\n/`await`\n\nor `Task.Run()`\n\ncan schedule work *implicitly* to run on the thread pool). In contrast, you need to *explicitly* schedule work to run on a Web Worker.\n\nIt has been possible to use Web Workers with Blazor (or pure WASM) .NET applications since .NET 8, as described in [these](https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-with-dotnet-on-web-workers) [articles](https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-on-webworkers), but they're somewhat complex in terms of the number of moving parts to get it working. There's also an open source project, [BlazorWorker](https://github.com/Tewr/BlazorWorker), that aims to encapsulate this with a simple API.\n\nIn .NET 11, a Web Worker project template has been added that similarly provides the bulk of the required code for Web Worker, to make it easier to run CPU intensive work on a background thread. For the rest of this post we'll look at how to use that project template, and what it contains.\n\n[Adding the Web Worker project](#adding-the-web-worker-project)\n\nIn this section, I show how to add the Web Worker template to a solution, how to reference it from your Blazor app, and how to use it to run code on a Web Worker.\n\n[Creating the initial Blazor app](#creating-the-initial-blazor-app)\n\nWe'll start by creating a basic Blazor WASM app. This is just the default template app that includes counter and weather forecast pages.\n\nThe following is a simple script that creates this app called `BlazorWebApp`\n\n, places it in the `/src/BlazorWebApp`\n\nsub-folder, creates a *.sln* file, and adds the project to it:\n\n```\ndotnet new sln\nmkdir src\ndotnet new blazorwasm -o ./src/BlazorWebApp\ndotnet sln add  ./src/BlazorWebApp\n```\n\nIf you run the app using `dotnet run`\n\n, you'll get the familiar Blazor app:\n\nWe're going to change how the forecasts are loaded. Currently, they're loading using an HTTP request for static JSON data. In our new approach, we'll invoke a method on a Web Worker which generates the data instead.\n\n[Generating the Web Worker template](#generating-the-web-worker-template)\n\nThe first thing to do is to generate the Web Worker project. Note that this template is intended to be used as a standalone project, which you then reference in your main app, rather than by adding the template *directly* to your Blazor project.\n\nThis differs from both\n\n[the existing documentation]on how to use Web Workers and[the BlazorWorker project].\n\nThe following creates the `BlazorWebWorker`\n\nproject as a sibling project, and adds it to the solution:\n\n```\ndotnet new webworker -o ./src/BlazorWebWorker\ndotnet sln add  ./src/BlazorWebWorker\n```\n\nThis template consists of just a few files, as you can see below:\n\nWe'll look in detail at the content of these files later. For now, we'll just look at how to *use* this project in your Blazor app.\n\n[Updating the Blazor app to reference the Web Worker project](#updating-the-blazor-app-to-reference-the-web-worker-project)\n\nThe Web Worker template creates a separate project, so you need to reference it from your *main* app:\n\n```\ndotnet add ./src/BlazorWebApp reference ./src/BlazorWebWorker\n```\n\nWe're also going to use the `[JSExport]`\n\nattribute, which means we need to explicitly enable unsafe code by adding `<AllowUnsafeBlocks>`\n\nto the project file:\n\n```\n<Project Sdk=\"Microsoft.NET.Sdk.BlazorWebAssembly\">\n\n  <PropertyGroup>\n    <TargetFramework>net11.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>\n    <!-- 👇 Add this -->\n    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.AspNetCore.Components.WebAssembly\" Version=\"11.0.0-preview.3.26207.106\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Components.WebAssembly.DevServer\" Version=\"11.0.0-preview.3.26207.106\" PrivateAssets=\"all\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\BlazorWebWorker\\BlazorWebWorker.csproj\" />\n  </ItemGroup>\n\n</Project>\n```\n\nOk, that's all the legwork, now let's add the code to actually *use* the Web Worker.\n\n[Defining the functions to run in the Web Worker](#defining-the-functions-to-run-in-the-web-worker)\n\nThe functions that you want to run in the Web Worker need to be exported using `[JSExport]`\n\n, and can only return primitive types (like `int`\n\nor `bool`\n\n) or `string`\n\ns. The following example shows a couple of important points:\n\n- The type containing the methods is a\n`static partial`\n\n. - It's decorated with\n`[SupportedOSPlatform(\"browser\")]`\n\n, because this`[JSExport]`\n\ncode is only supported to run in the browser. - The method is decorated with\n`[JSExport]`\n\n, which generates the code for interacting with JavaScript APIs from .NET code. You can read more about it in a[previous post](/running-dotnet-in-the-browser-without-blazor/)about running code in .NET without Blazor. - We can't return\n`WeatherForecast`\n\nobjects directly, they have to be serialized to a`string`\n\nfirst.\n\nHere's the code that simulates us doing some \"real\" work on the worker thread, and returning a serialized collection of objects\n\n```\n[SupportedOSPlatform(\"browser\")]\npublic static partial class WorkerMethods\n{\n    [JSExport]\n    public static string GetForecasts(int count)\n    {\n        // simulate doing \"real\" work that would block\n        // the UI if run on the main thread.\n        Thread.Sleep(5_000); \n\n        // Generate the data to return\n        Weather.WeatherForecast[] forecasts =\n        [\n            new()\n            {\n                Date = new DateOnly(2022, 01, 06),\n                TemperatureC = 1,\n                Summary = \"Freezing\",\n            },\n            new()\n            {\n                Date = new DateOnly(2022, 01, 07),\n                TemperatureC = 14,\n                Summary = \"Bracing\",\n            },\n            new()\n            {\n                Date = new DateOnly(2022, 01, 08),\n                TemperatureC = -13,\n                Summary = \"Freezing\",\n            },\n            new()\n            {\n                Date = new DateOnly(2022, 01, 09),\n                TemperatureC = -16,\n                Summary = \"Balmy\",\n            },\n            new()\n            {\n                Date = new DateOnly(2022, 01, 10),\n                TemperatureC = -2,\n                Summary = \"Chilly\",\n            }\n        ];\n\n        // Can't return it directly, must serialize to a string first\n        return JsonSerializer.Serialize(forecasts.Take(count)); \n    }\n}\n```\n\nOk, we have a method to run on our web worker, all that remains is to hook it up!\n\n[Creating a web worker and running code on it](#creating-a-web-worker-and-running-code-on-it)\n\nCurrently the `Weather`\n\npage component app uses an `HttpClient`\n\nto load the collection of `WeatherForecast`\n\nobjects:\n\n```\n@code {\n    private WeatherForecast[]? forecasts;\n\n    protected override async Task OnInitializedAsync()\n    {\n        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>(\"sample-data/weather.json\");\n    }\n}\n```\n\nWe can now rewrite this to use our web worker instead:\n\n```\n@using BlazorWebWorker\n@inject IJSRuntime JsRuntime\n@code {\n    private WeatherForecast[]? forecasts;\n\n    protected override async Task OnInitializedAsync()\n    {\n        // Create the webWorker\n        await using var worker = await WebWorkerClient.CreateAsync(JsRuntime);\n\n        // Get the full name of the method to invoke \n        // i.e. \"BlazorWebApp.WorkerMethods.GetForecasts\"\n        const string workerMethod = $\"{nameof(BlazorWebApp)}.{nameof(WorkerMethods)}.{nameof(WorkerMethods.GetForecasts)}\";\n        const int initialCount = 5;\n        \n        // invoke the code on the web worker\n        forecasts = await worker.InvokeAsync<WeatherForecast[]>(workerMethod, args: [initialCount]);\n    }\n}\n```\n\nNote that the `InvokeAsync<T>`\n\nmethod will automatically deserialize the `string`\n\nthat we return from `GetForecasts()`\n\ninto the `WeatherForecast[]`\n\nwe need.\n\n[Caching the web worker](#caching-the-web-worker)\n\nIf you run the above code, it will work, and the UI won't freeze. But it will also take a little while to startup. That's because the web worker has to initialize the .NET runtime, as it's isolated from the rest of your app. If you have dev tools open you'll see a bunch of .NET assemblies being requested when you start the Web Worker:\n\nNeeding to start up the runtime makes the `CreateAsync()`\n\ncall relatively slow, so if you had to do that *every* time you wanted to run some web worker code, you'd be taking quite the hit. Luckily, we can instead *cache* the Web Worker reference somewhere and reuse it (in the example below I just cache it in a `static`\n\nfield, but there are many other options):\n\n```\nprivate static WebWorkerClient? _client;\npublic static async Task<WebWorkerClient> GetOrCreateClient(IJSRuntime jsRuntime)\n{\n    // if we already have a client, use it\n    if (_client is { } client)\n    {\n        return client;\n    }\n\n    // Otherwise, initialize, which could be slow\n    _client = await WebWorkerClient.CreateAsync(jsRuntime);\n    return _client;\n}\n```\n\nand then in our `Weather`\n\npage, we can call this method instead:\n\n``` js\nprotected override async Task OnInitializedAsync()\n{\n-    await using var worker = await WebWorkerClient.CreateAsync(JsRuntime);\n+    var worker = await WorkerMethods.GetOrCreateClient(JsRuntime);\n}\n```\n\nAnd that's about all there is to it! The code runs on the web worker, and on subsequent calls you don't pay the overhead of starting the worker, because it's already running!\n\nNow that we know how to use the template, let's take a look at the code that's actually part of the template.\n\n[Looking at the Web Worker code](#looking-at-the-web-worker-code)\n\nThere's only a few files that are part of the template, so we'll look at each of them in turn to understand how it works.\n\n`WebWorkerClient`\n\nmanages things on the .NET side\n\n`WebWorkerClient`\n\nmanages things on the .NET sideWe'll start with the `WebWorkerClient`\n\n, as it's not too complex. I've annotated the code below with a rough explanation of how it works:\n\n```\npublic sealed class WebWorkerClient(IJSObjectReference worker) : IAsyncDisposable\n{\n    public static async Task<WebWorkerClient> CreateAsync(IJSRuntime jsRuntime)\n    {\n        // import the dotnet-web-worker-client.js code and take a reference\n        await using var module = await jsRuntime.InvokeAsync<IJSObjectReference>(\n            \"import\", \"./_content/BlazorWebWorker/dotnet-web-worker-client.js\");\n\n        // call the `create()` method to create an instance of\n        // the JavaScript `DotnetWebWorkerClient` object\n        var workerRef = await module.InvokeAsync<IJSObjectReference>(\"create\");\n\n        return new WebWorkerClient(workerRef);\n    }\n\n    public async Task<TResult> InvokeAsync<TResult>(string method, object[] args, CancellationToken cancellationToken = default)\n    {\n        // Call the `invoke` method on the WebWorker code,\n        // specifying the method to execute\n        return await worker.InvokeAsync<TResult>(\"invoke\", cancellationToken, [method, args]);\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        try\n        {\n            // Call the `terminate` method on the `DotnetWebWorkerClient` object\n            await worker.InvokeVoidAsync(\"terminate\");\n        }\n        catch (JSDisconnectedException)\n        {\n            // Circuit disconnected, worker is already gone\n        }\n\n        await worker.DisposeAsync();\n    }\n}\n```\n\nSo this is relatively simple - it imports the `dotnet-web-worker-client.js`\n\nmodule, calls `create`\n\n, and then allows calling `invoke`\n\nto run code on the Web Worker.\n\nNote that that \"path\" to the JavaScript file has the name of the project,\n\n`BlazorWebWorker`\n\n, embedded in it. This is the path where the content files will end up when your main Blazor app references this project. If you rename the project, or otherwise move these files, you'll need to fix this path.\n\nNow let's take a look at the content of the `dotnet-web-worker-client.js`\n\nfile itself.\n\n`dotnet-web-worker-client.js`\n\ncode sets up the Web Worker\n\n`dotnet-web-worker-client.js`\n\ncode sets up the Web WorkerThe `dotnet-web-worker-client.js`\n\nJavaScript file acts as a factory for creating a Web Worker, configuring it to start the .NET runtime, and for running functions on the worker by posting messages to it. I've added comments to the file below to explain what's going on, but it's generally pretty self explanatory.\n\n```\nclass DotnetWebWorkerClient {\n    #worker;\n    #pendingRequests = {};\n    #requestId = 0;\n\n    constructor(worker) {\n        this.#worker = worker;\n    }\n\n    // Invoked from .NET code to configure a web worker\n    static create() {\n        return new Promise((resolve, reject) => {\n            // Run the dotnet-web-worker.js file using the Web Workers API\n            const worker = new Worker('_content/BlazorWebWorker/dotnet-web-worker.js', { type: \"module\" });\n\n            // If the worker errors, bubble that up to the caller\n            worker.addEventListener('error', (e) => {\n                reject(new Error(e.message || 'Worker encountered an error'));\n            });\n\n            // Listen for the 'ready' message from the web worker.\n            worker.addEventListener('message', function onMessage(e) {\n                if (e.data.type === \"ready\") {\n                    // Once received, detach the 'ready' listener\n                    worker.removeEventListener('message', onMessage);\n                    if (e.data.error) {\n                        // An error occured during initialization, bubble it up\n                        reject(new Error(e.data.error));\n                    } else {\n                        // Succesfully initialized, create a wrapper, and setup communication\n                        const client = new DotnetWebWorkerClient(worker);\n                        client.#setupMessageHandler();\n                        // Return the wrapper to the caller\n                        resolve(client);\n                    }\n                }\n            });\n\n            // Initialize the worker with the path to the .NET runtime\n            const dotnetJsUrl = DotnetWebWorkerClient.#resolveDotnetJsUrl();\n            worker.postMessage({ type: 'init', dotnetJsUrl });\n        });\n    }\n\n    static #resolveDotnetJsUrl() {\n        // Resolve using the browser's import map (handles fingerprinted URLs in published apps).\n        // Workers don't inherit the page's import map, so we resolve on the main thread and pass the URL.\n        const dotnetJsUrl = new URL('_framework/dotnet.js', document.baseURI).href;\n        return import.meta.resolve?.(dotnetJsUrl) ?? dotnetJsUrl;\n    }\n\n    // Invoked by the .NET code to run a function in the worker\n    invoke(method, args) {\n        return new Promise((resolve, reject) => {\n            // Store the request for later handling and post it to the Web Worker\n            const id = ++this.#requestId;\n            this.#pendingRequests[id] = { resolve: r => resolve(this.#parseIfJson(r)), reject };\n            this.#worker.postMessage({ method, args, requestId: id });\n        });\n    }\n\n    // Convenience method for deserializing a string response returned\n    // from a Web Worker method invocation into JSON\n    #parseIfJson(value) {\n        if (typeof value === 'string') {\n            try {\n                return JSON.parse(value);\n            } catch {\n                // not JSON, return as-is\n            }\n        }\n        return value;\n    }\n\n    terminate() {\n        this.#rejectAllPending(\"Worker terminated\");\n        this.#worker?.terminate();\n        this.#worker = null;\n    }\n\n    // Setup handling of messages coming from the Web Worker\n    // in response to method invocations\n    #setupMessageHandler() {\n        this.#worker.addEventListener('message', (e) => {\n            if (e.data.type === \"result\") {\n                // Find the request in the stored collection\n                const request = this.#pendingRequests[e.data.requestId];\n                if (request) {\n                    delete this.#pendingRequests[e.data.requestId];\n                    // Return the method response or raise an error as appropriate\n                    if (e.data.error) {\n                        request.reject(new Error(e.data.error));\n                    } else {\n                        request.resolve(e.data.result);\n                    }\n                }\n            }\n        });\n\n        this.#worker.addEventListener('error', (e) => {\n            this.#rejectAllPending(e.message || 'Worker error');\n        });\n    }\n\n    // Cleanup pending requests when termination the worker\n    #rejectAllPending(errorMessage) {\n        for (const id in this.#pendingRequests) {\n            this.#pendingRequests[id].reject(new Error(errorMessage));\n            delete this.#pendingRequests[id];\n        }\n    }\n}\n\nexport function create() {\n    return DotnetWebWorkerClient.create();\n}\n```\n\nSo this module exposes a single `create()`\n\nmethod that creates a Web Worker that runs the `dotnet-web-worker.js`\n\ncode, and configures message handling, so that messages can be sent to the Web Worker and responses returned. Due to the pub-sub nature of this message passing, there's a small amount of book-keeping required to associate responses with a given request, but otherwise there's not much more happening.\n\nOne interesting point of this code for me was the use of\n\n`#`\n\nprefixed members indicating[. These work just like]private elements`private`\n\nmembers in C#, but I didn't realise they existed, despite being \"available across browsers since July 2021\"! I guess that shows how long it's been since I was writing JavaScript! 😅\n\nThe JavaScript above is the \"glue\" code between your application and the Web Worker, but the Web Worker itself is running `dotnet-web-worker.js`\n\n. In the next section we'll take a look at that code too.\n\n`dotnet-web-worker.js`\n\nruns as a Web Worker\n\n`dotnet-web-worker.js`\n\nruns as a Web WorkerWe've already seen that the `dotnet-web-worker-client.js`\n\ncode runs in the context of your app and starts a Web Worker that runs `dotnet-web-worker.js`\n\n. The Web Worker code is essentially isolated from your app, so it needs to initialize a whole new instance of .NET before it can run your code. The following is the entirety of the Web Worker code, annotated to explain what's going on.\n\n``` js\nlet workerExports = null;\nlet startupError = null;\n\nasync function initialize(dotnetJsUrl) {\n    try {\n        // Try to import the _framework/dotnet.js file so\n        // that we can we run .NET code in WASM\n        const { dotnet } = await import(dotnetJsUrl);\n\n        // Initialize the .NET runtime. For more on this, see\n        // https://andrewlock.net/running-dotnet-in-the-browser-without-blazor/\n        const { getAssemblyExports, getConfig } = await dotnet.create();\n        const assemblyName = getConfig().mainAssemblyName;\n\n        // Get the methods we're allowed to run\n        // i.e. anything that has `[JSExport]`\n        workerExports = await getAssemblyExports(assemblyName);\n\n        // Post a message back to the main app to indicate\n        // we are ready to handle \"invoke\" messages\n        self.postMessage({ type: \"ready\" });\n    } catch (err) {\n        // Something went wrong, report the error back to the main app\n        const errorMessage = err?.message ?? String(err);\n        startupError = errorMessage;\n        console.error(\"[Worker] Failed to initialize .NET:\", err);\n        self.postMessage({ type: \"ready\", error: errorMessage });\n    }\n}\n\n// Listen for messages from the main app\nself.addEventListener('message', async (e) => {\n    if (e.data.type === 'init') {\n        // Initialization method received from the main app\n        // so start the .NET runtime \n        await initialize(e.data.dotnetJsUrl);\n        return;\n    }\n\n    // Deconstruct the data, to work out which [JSExport] method to run\n    const { method, args, requestId } = e.data;\n\n    try {\n        if (!workerExports) {\n            // The .NET runtime wasn't initialized yet, shouldn't happen in practice\n            throw new Error(startupError || \"Worker .NET runtime not loaded\");\n        }\n\n        // Find the [JSExport] method to invoke\n        const fn = method.split('.').reduce((obj, part) => obj?.[part], workerExports);\n        if (typeof fn !== 'function') {\n            throw new Error(`Method not found: ${method}`);\n        }\n\n        // Invoke the method\n        const result = await fn(...args);\n\n        // Post a message back to the main app with the result of the function!\n        self.postMessage({ type: \"result\", requestId, result }, collectTransferables(result));\n    } catch (err) {\n        // Post a message back to the main app with the error that occured\n        self.postMessage({ type: \"result\", requestId, error: err?.message ?? String(err) });\n    }\n});\n\n// Transferable objects are a way to avoid copying data\n// when posting messages to a Web Worker. For more details, see\n// https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage#transfer\nfunction collectTransferables(value) {\n    if (ArrayBuffer.isView(value)) return [value.buffer];\n    if (value instanceof ArrayBuffer) return [value];\n    return [];\n}\n```\n\nThe code in this function starts a new instance of the .NET runtime when initialized (without using Blazor—[see here for more details](running-dotnet-in-the-browser-without-blazor/)). This is a big part of the overhead associated with using a Web Worker, and is why you should try to \"reuse\" the Web Worker instance if possible. Every time you start a new Worker, the runtime has to startup, download all its dependencies, and initialize, which is a relatively large amount of work.\n\nOnce the .NET runtime has started, the Web Worker just sits waiting for messages to be sent, asking for methods to execute. It then simply runs them and posts the messages back. Simple!\n\nAnd that's all there is to the Web Worker mechanism. There's no doubt more we *could* go into, such as thinking about [transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) or avoiding needing a separate project, but this post is plenty long enough!\n\n[Summary](#summary)\n\nIn this post, I described the new `webworker`\n\ntemplate that shipped in .NET 11 preview 2, which allows running .NET code in a Web Worker when you're running code in the browser using Blazor ([or even without Blazor](/running-dotnet-in-the-browser-without-blazor/)).\n\nI started by explaining that the main reason to do this is to run computationally expensive code *without* blocking the UI thread. I then showed how to use the new `webworker`\n\ntemplate to create a standalone project that you can add to your Blazor app, so that specific methods can run on a Web Worker. Finally I walked through the code contained in the template, to understand exactly what's happening behind the scenes.", "url": "https://wpnews.pro/news/running-background-tasks-in-blazor-with-web-workers-exploring-the-net-11-preview", "canonical_source": "https://andrewlock.net/exploring-the-dotnet-11-preview-1-running-background-tasks-in-blazor-with-web-workers/", "published_at": "2026-05-12 10:00:00+00:00", "updated_at": "2026-05-23 21:35:57.517757+00:00", "lang": "en", "topics": ["developer-tools"], "entities": [".NET", "Blazor", "Web Workers", "JavaScript", "V8", "WebAssembly", "Windows Forms"], "alternates": {"html": "https://wpnews.pro/news/running-background-tasks-in-blazor-with-web-workers-exploring-the-net-11-preview", "markdown": "https://wpnews.pro/news/running-background-tasks-in-blazor-with-web-workers-exploring-the-net-11-preview.md", "text": "https://wpnews.pro/news/running-background-tasks-in-blazor-with-web-workers-exploring-the-net-11-preview.txt", "jsonld": "https://wpnews.pro/news/running-background-tasks-in-blazor-with-web-workers-exploring-the-net-11-preview.jsonld"}}