Why Blazor Needs JavaScript Interop
JavaScript interop, or JS interop, is the bridge Blazor uses whenever functionality lives only in the browser and has no .NET equivalent, such as reading the clipboard, drawing to a canvas element, integrating a JS charting library, or triggering a file download, since Blazor's own rendering engine has no direct access to arbitrary browser APIs. Both hosting models route interop calls through IJSRuntime, but Blazor Server sends each call over the existing SignalR connection, adding a network round trip that WASM's in-process calls don't incur.
Cricket analogy: A translator is needed when an English-speaking commentator interviews a player who only speaks Hindi fluently, just as JS interop translates between the .NET runtime and browser APIs that speak only JavaScript.
Calling JS from C# with IJSRuntime
Calling JavaScript from C# uses IJSRuntime.InvokeAsync<TResult>(identifier, args), where identifier names a globally accessible JS function, typically one defined in a wwwroot/*.js file referenced via a script tag in index.html or App.razor, and args are automatically JSON-serialized. Since .NET 5, IJSObjectReference lets you scope a call to a specific JS module loaded via import(), avoiding global namespace pollution, and since .NET 7's IJSInProcessRuntime, WASM apps can make synchronous JS calls when the async round trip isn't needed.
Cricket analogy: A captain calls a specific fielder by name to make a play, not just shouting 'someone catch it,' similar to how InvokeAsync targets a specific named JS function rather than something ambiguous.
// wwwroot/js/canvasInterop.js
export function drawCircle(canvasId, x, y, radius) {
const ctx = document.getElementById(canvasId).getContext('2d');
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.stroke();
}@inject IJSRuntime JS
@code {
private IJSObjectReference? _module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./js/canvasInterop.js");
await _module.InvokeVoidAsync("drawCircle", "myCanvas", 100, 100, 40);
}
}
}Calling C# from JS with JSInvokable
The reverse direction—JavaScript calling back into C#—requires a static method decorated with [JSInvokable], invoked from JS via DotNet.invokeMethodAsync('AssemblyName', 'MethodName', ...args), or an instance method invoked through a DotNetObjectReference created with DotNetObjectReference.Create(this) and passed into the JS call, which is the pattern used for callbacks like a JS event listener notifying a Blazor component when a canvas drawing gesture completes. The DotNetObjectReference must be explicitly disposed to release the .NET-side reference once the JS side no longer needs it.
Cricket analogy: A stadium's third umpire radios a decision back down to the on-field umpire after review, closing the loop the other direction, similar to how JS calls back into C# via DotNet.invokeMethodAsync to complete the round trip.
[JSInvokable]
public static Task<string> NotifyDrawingComplete(string canvasId)
{
Console.WriteLine($"Drawing on {canvasId} finished");
return Task.FromResult("acknowledged");
}DotNet.invokeMethodAsync('MyApp', 'NotifyDrawingComplete', 'myCanvas')
.then(result => console.log(result));Loading JS as an ES module via await JS.InvokeAsync<IJSObjectReference>("import", "./js/canvasInterop.js") avoids polluting the global window object with function names, and the returned IJSObjectReference should be disposed (it implements IAsyncDisposable) when the component no longer needs the module.
Every argument and return value crossing the JS interop boundary is JSON-serialized, so types like DateTime, decimal precision beyond what JS numbers support, or circular object references can silently lose fidelity or throw serialization errors — test interop calls with the actual data shapes you'll send, not just simple strings and ints.
- JS interop bridges functionality only available in the browser (canvas, clipboard, third-party JS libraries) that .NET can't reach directly
- IJSRuntime.InvokeAsync<TResult> calls a named JS function and JSON-serializes the arguments
- IJSObjectReference scopes calls to an imported JS module, avoiding global namespace pollution
- [JSInvokable] exposes a static or instance C# method that JavaScript can call back into
- DotNetObjectReference.Create(this) is required to let JS call back into a specific component instance
- DotNetObjectReference must be explicitly disposed to release the .NET-side reference
- Blazor Server interop calls travel over the existing SignalR connection, adding latency WASM doesn't have
Practice what you learned
1. Why does Blazor need JS interop at all?
2. What does IJSObjectReference help avoid compared to calling global JS functions directly?
3. What attribute must a C# method have for JavaScript to call it via DotNet.invokeMethodAsync?
4. Why must DotNetObjectReference.Create(this) results be explicitly disposed?
5. Why might a Blazor Server app feel slightly slower on heavy JS interop usage than an equivalent WASM app?
Was this page helpful?
You May Also Like
Local Storage and Session Storage
Persist Blazor app state in the browser using localStorage and sessionStorage via JS interop or community libraries.
State Management Patterns
Explore techniques for sharing and persisting state across Blazor components, from cascading parameters to dedicated state containers.
HttpClient and Calling APIs
Learn how to configure and use HttpClient in Blazor Server and WebAssembly to call REST APIs safely and efficiently.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics