WebAssembly is an exciting new language that many JavaScript engines have added support for. WebAssembly promises to make it much easier to compile languages like C and C++ to something that runs in the browser. However, I'm most excited about the ability to write optimized custom arithmetic and buffer manipulations, like, say, fast decimal floating point arithmetic in JavaScript without having to wait for TC39 to get around to it. In this article, I'll show you how to get a couple rudimentary WebAssembly examples running in Node.js, and run a couple trivial benchmarks to show the performance impact.

Note: The code in this article was only tested on Node 7.2.1 with the --expose-wasm flag. The code will not work on Node 6.x or Node 7.6.0, and will not work without the --expose-wasm flag.

What is WebAssembly Anyway?

The --expose-wasm flag gives you access to a global object Wasm which has several helper functions for creating WebAssembly modules. For the purposes of this article, a WebAssembly module is just a collection of functions written in WebAssembly.

$ ~/Workspace/node-v7.2.1-linux-x64/bin/node --expose-wasm
> Wasm
{ verifyModule: [Function],
  verifyFunction: [Function],
  instantiateModule: [Function],
  experimentalVersion: 11 }
>

To create a WebAssembly module, you need to call Wasm.instantiateModule() with a Uint8Array that represents the module. Below is an example of instantiating an empty WebAssembly module.

$ ~/Workspace/node-v7.2.1-linux-x64/bin/node --expose-wasm
> Wasm.instantiateModule(new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x0b, 0x00, 0x00, 0x00]));
{}
>

So at the basic level, creating a WebAssembly module consists of putting the correct hex digits into the instantiateModule() function. What do these hex numbers mean? These hex numbers are the preamble that every .wasm file starts with (.wasm is the canonical extension for WebAssembly files). Every WebAssembly file must have these bytes, so this is the minimum viable WebAssembly module.

Adding Two Numbers

Thankfully, you don't have to write the bytes yourself. There's plenty of compilers out there for compiling C, C++, and even Rust to WebAssembly. There's also an intermediate format called "WebAssembly AST", or "wast" for short. Here's what a function that returns the sum of its 2 parameters looks like in wast:

(module
  (func $addTwo (param i32 i32) (result i32)
    (i32.add
      (get_local 0)
      (get_local 1)))
  (export "addTwo" $addTwo))

You can use this online tool to compile wast code down into the wasm binary, or you can just download the compiled .wasm from me.

Next, how do you use a .wasm file in Node.js? In order to use the .wasm, you need to load the file and convert the Node.js buffer that node's fs library returns into an ArrayBuffer.

const fs = require('fs');
const buf = fs.readFileSync('./addTwo.wasm');
const lib = Wasm.instantiateModule(toUint8Array(buf)).exports;

// `Wasm` does **not** understand node buffers, but thankfully a node buffer
// is easy to convert to a native Uint8Array.
function toUint8Array(buf) {
  var u = new Uint8Array(buf.length);
  for (var i = 0; i < buf.length; ++i) {
    u[i] = buf[i];
  }
  return u;
}

console.log(lib.addTwo(2, 2)); // Prints '4'
console.log(lib.addTwo.toString()); // Prints 'function addTwo() { [native code] }'

How fast is addTwo in WebAssembly versus a plain old JavaScript implementation? Here's a trivial benchmark:

const fs = require('fs');
const buf = fs.readFileSync('./addTwo.wasm');
const lib = Wasm.instantiateModule(toUint8Array(buf)).exports;

const Benchmark = require('benchmark');

const suite = new Benchmark.Suite;

suite.
  add('wasm', function() {
    lib.addTwo(2, 2);
  }).
  add('js', function() {
    addTwo(2, 2);
  }).
  on('cycle', function(event) {
    console.log(String(event.target));
  }).
  on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  }).
  run();

function addTwo(a, b) {
  return a + b;
}

function toUint8Array(buf) {
  var u = new Uint8Array(buf.length);
  for (var i = 0; i < buf.length; ++i) {
    u[i] = buf[i];
  }
  return u;
}
$ ~/Workspace/node-v7.2.1-linux-x64/bin/node --expose-wasm ./addTwo.js
4
wasm x 43,497,742 ops/sec ±0.77% (88 runs sampled)
js x 66,021,200 ops/sec ±1.28% (83 runs sampled)
Fastest is js

Factorial

WebAssembly doesn't have any performance benefit over plain old JS in the above example. Let's do something a little more complex: computing factorials recursively. Here's a .wast file that exposes a fac() function which computes factorial recursively.

(module
  (func $fac (param i32) (result i32)
    (if (i32.lt_s (get_local 0) (i32.const 1))
      (then (i32.const 1))
      (else
        (i32.mul
          (get_local 0)
          (call $fac
            (i32.sub
              (get_local 0)
              (i32.const 1)))))))
  (export "fac" $fac))

You can use this tool) to compile the .wasm or just download it here.

Below is another trivial benchmark comparing computing 100! with WebAssembly versus with JavaScript:

const fs = require('fs');
const buf = fs.readFileSync('./factorial.wasm');
const lib = Wasm.instantiateModule(toArrayBuffer(buf)).exports;

const Benchmark = require('benchmark');

const suite = new Benchmark.Suite;

suite.
  add('wasm', function() {
    lib.fac(100);
  }).
  add('js', function() {
    fac(100);
  }).
  on('cycle', function(event) {
    console.log(String(event.target));
  }).
  on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  }).
  run();

function fac(n) {
  if (n <= 0) {
    return 1;
  }
  // `x | 0` rounds down, so `2.0001 | 0 === 2`. This helps deal with floating point precision issues like `0.1 + 0.2 !== 0.3`
  return (n * fac(n - 1)) | 0;
}

function toArrayBuffer(buf) {
  var ab = new ArrayBuffer(buf.length);
  var view = new Uint8Array(ab);
  for (var i = 0; i < buf.length; ++i) {
    view[i] = buf[i];
  }
  return ab;
}
$ ~/Workspace/node-v7.2.1-linux-x64/bin/node --expose-wasm ./factorial.js
wasm x 2,484,967 ops/sec ±2.09% (87 runs sampled)
js x 1,088,426 ops/sec ±2.63% (80 runs sampled)
Fastest is wasm
$

Moving On

In these rudimentary examples, WebAssembly shows promise in terms of allowing you to really optimize JS code. My benchmarks are quite rudimentary and WebAssembly is still unstable and not well adopted, so don't rush to try to write your next web app in wast. However, now's the time to play with WebAssembly, especially since it is available in Node.js.

I wrote this article (and many others) after a dose of Ciltep (non-affiliate link here). Ciltep helps you stay focused and learn fast, without jitters or crashes. Give it a shot before you take a deep dive into WebAssembly.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus