Myth: Calling from JavaScript to WebAssembly and vice-versa is expensive
Every call to a WebAssembly function crosses the FFI boundary, does that add up as measurable overhead?
We can compare an identity function implemented in JavaScript directly against a Rust identity
function.
The JavaScript identity never leaves JavaScript:
function identity(val: unknown): unknown {
return val;
}
The WebAssembly identity crosses the bridge on every call:
#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn identity(val: JsValue) -> JsValue {
val
}
}
Both benchmarks receive a pre-generated string of length \(N\) and call their respective identity function once. Generation is unmeasured.
Results for this benchmark are extremely noisy because in both cases the computation is almost instant regardless of the length of the generated string being passed in.
Did you notice that we are passing a generated string through WebAssembly using a
JsValuetype? This type allowswasm-bindgento pass a lightweight reference to the JavaScript string and entirely skip copying the JavaScript string into WebAssembly’s linear memory.
However, we don’t need many benchmarks to prove that WebAssembly functions are extremely optimised. All the way back in 2018, one year before WebAssembly was made the fourth language of the Web, calls between JavaScript and WebAssembly became fast.
In the more recent times JavaScript engines are continuously adding optimisations to WebAssembly,
for example V8, the JavaScript engine running in Chrome can speculatively inline all WebAssembly
function call instructions, e.g. call, call_indirect, and call_ref.
Thus we can safely say, myth busted!