Open
Description
I'm not sure if that's by design or a bug or I'm doing something wrong, but [JSExport/Import]
interop in .NET 9 AOT is about 10 times slower than in Rust. Even considering the runtime overhead, 10x difference seems just too much.
Below are the samples I've used for the benchmark:
C#
.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configuration>Release</Configuration>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RunAOTCompilation>true</RunAOTCompilation>
<OptimizationPreference>Speed</OptimizationPreference>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.Versioning.SupportedOSPlatform">
<_Parameter1>browser</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
Program.cs
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using System.Text.Json.Serialization;
public struct Data
{
public string Info;
public bool Ok;
public int Revision;
public string[] Messages;
}
[JsonSerializable(typeof(Data))]
internal partial class SourceGenerationContext : JsonSerializerContext;
public static partial class Program
{
public static void Main () { }
[JSExport]
public static int EchoNumber ()
{
return GetNumber();
}
[JSExport]
public static string EchoStruct ()
{
var json = GetStruct();
var data = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.Data);
return JsonSerializer.Serialize(data, SourceGenerationContext.Default.Data);
}
[JSImport("getNumber", "x")]
private static partial int GetNumber ();
[JSImport("getStruct", "x")]
private static partial string GetStruct ();
}
main.mjs
import { dotnet } from "./bin/Release/net9.0/browser-wasm/AppBundle/_framework/dotnet.js";
const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
.withDiagnosticTracing(false)
.create();
setModuleImports("x", {
getNumber: () => 42,
getStruct: () => JSON.stringify({
Info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
Ok: true,
Revision: -112,
Messages: ["foo", "bar", "baz", "nya", "far"]
})
});
const exports = await getAssemblyExports(getConfig().mainAssemblyName);
await new Promise(res => setTimeout(res, 100));
let startTime = performance.now();
for (let i = 0; i < 100000; i++)
exports.Program.EchoNumber();
console.log(`Echo number: ${(performance.now() - startTime).toFixed(2)} ms`);
await new Promise(res => setTimeout(res, 100));
startTime = performance.now();
for (let i = 0; i < 100000; i++)
exports.Program.EchoStruct();
console.log(`Echo struct: ${(performance.now() - startTime).toFixed(2)} ms`);
Rust
Cargo.toml
[package]
name = "rust-wasm"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[profile.release]
codegen-units = 1
lto = true
opt-level = 3
panic = "abort"
strip = true
lib.rs
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Serialize, Deserialize)]
pub struct Data {
pub info: String,
pub ok: bool,
pub revision: i32,
pub messages: Vec<String>,
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = getNumber)]
fn get_number() -> i32;
#[wasm_bindgen(js_name = getStruct)]
fn get_struct() -> String;
}
#[wasm_bindgen(js_name = echoNumber)]
pub fn echo_number() -> i32 {
get_number()
}
#[wasm_bindgen(js_name = echoStruct)]
pub fn echo_struct() -> String {
let json = get_struct();
let data: Data = serde_json::from_str(&json).unwrap();
serde_json::to_string(&data).unwrap()
}
main.mjs
import { echoNumber, echoStruct } from './pkg/rust_wasm.js';
global.getNumber = () => 42;
global.getStruct = () => JSON.stringify({
info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
ok: true,
revision: -112,
messages: ["foo", "bar", "baz", "nya", "far"]
});
await new Promise(res => setTimeout(res, 100));
let startTime = performance.now();
for (let i = 0; i < 100000; i++)
echoNumber();
console.log(`Echo number: ${(performance.now() - startTime).toFixed(2)} ms`);
await new Promise(res => setTimeout(res, 100));
startTime = performance.now();
for (let i = 0; i < 100000; i++)
echoStruct();
console.log(`Echo struct: ${(performance.now() - startTime).toFixed(2)} ms`);
dotnet publish -c Release
node main.mjs
Echo number: 65.49 ms
Echo struct: 1082.17 ms
wasm-pack build --target nodejs
node main.mjs
Echo number: 5.91 ms
Echo struct: 238.33 ms
Is this something to be expected and can't be improved or maybe I'm missing some configuration/optimization options?