Blazor WASM's Deputy Thread Model Will Break JavaScript Interop - Here's Why That Matters
Source: Dev.to
The Problem
Microsoft is changing how .NET runs inside WebAssembly. When you enable threading with
truethe entire .NET runtime moves off the browser’s main thread and onto a background Web Worker — what they call the “Deputy Thread” model.
This sounds good on paper: the UI stays responsive and .NET gets real threads. Everyone wins—until it breaks JavaScript interop. Not in a subtle, edge‑case way, but fundamentally.
What Actually Happens
In traditional Blazor WASM (no threading), .NET and JavaScript share the same thread. When JavaScript calls DotNet.invokeMethod, the CPU jumps from the JS stack to the C# stack and back. It’s fast, synchronous, and works.
In the Deputy Thread model, .NET lives in a Web Worker while JavaScript lives on the UI thread. When JavaScript tries to call DotNet.invokeMethod, the UI thread would have to block while waiting for the worker to respond.
Browsers don’t allow that—the UI thread is forbidden from blocking on a worker. Consequently the .NET runtime throws:
Error: Cannot call synchronous C# methods.And that’s the end of synchronous JS‑to‑.NET communication.
Why “Just Use Async” Doesn’t Work
The most common response is: “just make everything async.” That misunderstands how the browser works. The JavaScript event model requires synchronous handling in many scenarios. These aren’t obscure edge cases; they’re core browser functionality.
event.preventDefault()
element.addEventListener('submit', (event) => {
// This MUST happen synchronously, right here, right now.
// If you await a response from a worker thread,
// the browser has already submitted the form.
event.preventDefault();
});You cannot await a response from a .NET worker and then call preventDefault(). By the time the worker responds, the browser has already processed the default action (form submission, navigation, drag completion, etc.).
event.stopImmediatePropagation()
Same constraint—other listeners have already fired by the time an async response arrives.
beforeunload
window.addEventListener('beforeunload', (event) => {
// Must return synchronously. No promises. No awaiting workers.
event.returnValue = 'Are you sure?';
});Synchronous Property Access
Many JavaScript APIs expose synchronous getters and setters. A C# wrapper that aims to match the JS API surface needs to read these values synchronously. In the Deputy Thread model, every property access becomes an async round‑trip to a worker.
The Real‑World Impact
I maintain SpawnDev.BlazorJS—a library that provides typed C# wrappers for JavaScript APIs in Blazor WebAssembly. It’s part of a 41‑package ecosystem with over 323 k total NuGet downloads, covering WebRTC, WebGPU, WebTorrent, Canvas, Crypto, IndexedDB, Streams, and dozens of other browser APIs.
The library exists because Microsoft’s built‑in JS interop is incomplete; SpawnDev.BlazorJS fills the gaps with high‑fidelity 1‑to‑1 mappings of the JavaScript specification.
The Deputy Thread model breaks this library. Not partially—any operation that requires DotNet.invokeMethod fails when .NET is on a worker.
But this isn’t just about my library. Any Blazor WASM project that:
- Handles DOM events requiring synchronous responses
- Wraps synchronous JavaScript APIs
- Uses synchronous callbacks from JS to .NET
- Builds real‑time applications (WebRTC, WebSocket handlers, canvas rendering)
…will hit this wall.
We Already Solved Multi‑Threading Without Breaking Interop
SpawnDev.BlazorJS.WebWorkers (90 k+ downloads) provides multi‑threading for Blazor WASM without the Deputy Thread model. Its architecture is straightforward:
- The main .NET instance stays on the browser’s UI thread.
- Synchronous JS interop works exactly as designed.
- Heavy computation is dispatched to background Web Workers explicitly.
- The developer controls which work runs where.
This is how threading should work in the browser: the main thread handles UI and synchronous JS communication, while workers handle heavy lifting. The developer decides what runs where.
Microsoft’s approach inverts this: move everything to a worker, then try to proxy the UI. It solves the “UI jank during heavy computation” problem, but it does so by severing the synchronous link between .NET and the browser.
What I’m Asking For
I’m not asking Microsoft to abandon the Deputy Thread model—it has legitimate value for apps that prioritize background computation over DOM fidelity. I’m asking for a choice:
true
true
In this mode:
Program.Mainruns on the browser’s UI thread → synchronous JS interop works normally.Task.Runand thread‑pool work dispatch to background Web Workers → blocking primitives (lock,Thread.Sleep) work on background threads and throw on the UI thread (the browser already enforces this).- Libraries that depend on synchronous interop continue to function.
Developers who want full Deputy Thread isolation can still opt into it, but it shouldn’t be the only option.
The Bigger Concern
My deeper worry is the trajectory. If the Deputy Thread becomes the only supported execution mode, a large class of Blazor WASM applications—those that rely on synchronous interop, real‑time UI handling, or thin wrappers around browser APIs—will be forced to abandon Blazor or rewrite substantial portions of their codebases. Providing an opt‑out preserves the existing ecosystem while still allowing the new model for scenarios that truly benefit from it.
l — even for single‑threaded builds — every Blazor WASM application that depends on synchronous JS interop will break. Not just SpawnDev. Every library. Every application.
The browser is a **local execution environment**, not a remote server. .NET in the browser should be able to talk to JavaScript the same way JavaScript talks to itself — synchronously when needed, asynchronously when preferred.
---
## Where This Is Being Discussed
- **[dotnet/aspnetcore#54365](https://github.com/dotnet/aspnetcore/issues/54365)** – “Make Blazor WebAssembly work on multithreaded runtime”
- **[dotnet/runtime](https://github.com/dotnet/runtime)** – Where the threading architecture decisions are actually made
If this affects your work, add your voice to those issues. The more the team hears from developers who depend on synchronous interop, the more likely we are to get a hybrid option.
---
*I’m **[Todd Tanner (@LostBeard)](https://github.com/LostBeard)**, author of the **[SpawnDev](https://www.nuget.org/profiles/LostBeard)** library ecosystem for Blazor WebAssembly. I’ve been building high‑performance browser applications with .NET for years, and I want to keep doing it.*