Optimizing JS Interop in a Blazor WebAssembly application

 
 
  • Gérald Barré

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.

Razor
@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:

C#
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.

Razor
@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

JavaScript
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
}
Razor
@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!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub