Optimizing JS Interop in a Blazor WebAssembly application
Blazor WebAssembly and Blazor Server allow calling JS functions from the .NET code. By using IJSRuntime.InvokeAsync<T>(name)
, you can invoke a JS function and get its result. This interface works well in Blazor WebAssembly and Blazor Server.
@inject IJSRuntime JSRuntime
@code{
protected override async Task OnInitializedAsync()
{
await JSRuntime.InvokeAsync<string>("MyFunction");
}
}
In Blazor WebAssembly, you have direct access to the actual JS runtime. You don't need any network round-trip compared to Blazor Server.
Being able to use the JS runtime directly means that you can avoid the abstraction and get better performance. To bypass the abstraction, you can use WebAssemblyJSRuntime
directly or IJSInProcessRuntime
. You can update the Main
method to allow get these types using dependency injection:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddSingleton(serviceProvider => (IJSInProcessRuntime)serviceProvider.GetRequiredService<IJSRuntime>());
builder.Services.AddSingleton(serviceProvider => (IJSUnmarshalledRuntime)serviceProvider.GetRequiredService<IJSRuntime>());
await builder.Build().RunAsync();
}
Using IJSInProcessRuntime
you don't need to use asynchronous code. This means you can avoid the state-machine generated by the C# compiler and improve the performance. It also simplifies the code you write as there is no need to use the await
keyword.
@inject IJSInProcessRuntime JSInProcessRuntime
@code{
protected override void OnInitialized()
{
// Synchronous call
JSInProcessRuntime.Invoke<string>("MyFunction");
}
}
The WebAssemblyJSRuntime
also provides a low-level method that doesn't marshal JS types. This means you have to use the mono methods to convert the types manually. This is true for the parameters
function MyUnmarshalledFunction(rawName) {
// Not documented and may change in future versions of mono, use it at your own risk...
// The current source code of the BINDING functions: https://github.com/mono/mono/blob/b6ef72c244bd33623d231ff05bc3d120ad36b4e9/sdks/wasm/src/binding_support.js
// These functions are defined by _framework/dotnet.<version>.js, which is imported by _framework/blazor.webassembly.js
const name = BINDING.conv_string(rawName); // Convert the handle to a JS string
return BINDING.js_to_mono_obj(`Hello ${name}!`); // Convert a JS object to a mono object that you can use in the .NET code
}
@inject IJSUnmarshalledRuntime JSUnmarshalledRuntime
@code{
protected override void OnInitialized()
{
JSUnmarshalledRuntime.InvokeUnmarshalled<string>("MyUnmarshalledFunction", "meziantou");
}
}
Here's the performance of each method for 1000 invocations:
If you are only using Blazor WebAssembly, you should use IJSInProcessRuntime.Invoke
when possible as it is almost 4 times faster than InvokeAsync
and it simplifies the code as there is no need to use await
everywhere. The InvokeUnmarshalled
method should only be used for the most advanced cases where the performance is critical as the JS functions available in BINDING
are not documented and may change in future versions of mono. This means your code may break when updating the application.
In the next post, we'll see how to use InvokeUnmarshalled
to drastically improve the performance of a specific JS method invocation. Stay tuned!
Do you have a question or a suggestion about this post? Contact me!