Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Why WebAssembly on the browser?

Although JavaScript can be written to be extremely fast, it’s non trivial to squeeze performance out of it. Often it requires writing the JavaScript like C code, and you still need to be extremely aware of performance cliffs that exist in the underlying JavaScript interpreters.

This book comes with a built-in benchmark runner so we can test directly in your browser. Because it runs on your specific hardware and browser engine, your results will be unique. If the data looks noisy, hit the ↺ Restart button.

To test out the benchmark system in pure JavaScript, let’s explicitly measure the performance degradation of function deoptimisation.

Although JavaScript is dynamically typed, object literals are still assigned a hidden class. So, declaring {a: 1, b: 2} in JavaScript gets a different hidden class from the object {b: 2, a: 1} even though the objects are otherwise identical. If a function is called with different hidden class arguments, it can deoptimise and become much slower to call.

Given this trivial function that sums some fields on a JavaScript object:

function sum_fields(obj) {
    return obj.a + obj.b + obj.c + obj.d + obj.e;
}

We can benchmark and compare the speed of the function based on simply what input objects are generated for the benchmark.

The monomorphic benchmark creates input data using a single factory — one shape, one hidden class, function is expected on the fast path:

const objectWithNumbersFactory = (a: number, b: number, c: number, d: number, e: number) => ({ a, b, c, d, e });

The megamorphic benchmark generates input data by randomly choosing from one of eight factories. Each factory generates the object literal with fields in different orders causing the function to deoptimise.

const objectWithNumbersFactories: Array<(a: number, b: number, c: number, d: number, e: number) => ObjectWithNumbers> = [
    (a, b, c, d, e) => ({ a, b, c, d, e }),
    (a, b, c, d, e) => ({ b, a, c, d, e }),
    (a, b, c, d, e) => ({ c, b, a, d, e }),
    (a, b, c, d, e) => ({ d, c, b, a, e }),
    (a, b, c, d, e) => ({ e, d, c, b, a }),
    (a, b, c, d, e) => ({ a, c, e, b, d }),
    (a, b, c, d, e) => ({ b, d, a, e, c }),
    (a, b, c, d, e) => ({ e, c, a, d, b }),
];

The graph below is calculated on your machine so may show slightly different results on each try.

The graph above shows that the inputs into otherwise identical functions can have huge impact on the performance of the function. In the worst case the megamorphic benchmark can be 10 times slower. In fact, these optimisations were specifically used to improve TypeScript’s compiler performance.

Unlike JavaScript, which contains these complex runtime behaviors and heuristics, WebAssembly is statically typed and compiled ahead of time. This allows WebAssembly to achieve its design goal of deterministic high performance.