We're hitting another wall. Not the wall we hit our heads against when looking at the weird things JavaScript does to what we thought was a commutative operator such as +:

[] + {} === {} + [] // Super false

The wall we are hitting is the one where we are trying to speed up a language like JavaScript even more than we previously have. JavaScript has been around since 1995 and was the native language of the web. Various improvements shortly after then had given small performance boosts to try and make client side code run faster. But it was a struggle and JavaScript remained pretty slow in browsers.

By 2008 Just In Time (JIT) compilers started to emerge as the browser arena became more competitive. We started to see up to 10x speed boosts in JavaScript on the client side. The acceleration then slowed and improvements only became small increments up until now.

WebAssembly may be (and most likely will be) the next breakthrough we need to take web based applications to the next level and offer huge performance wins.

WebAssembly is described as a binary instruction format for a stack-based virtual machine, which is meant to be a portable target for compilation of high-level languages. Think of it as wedging itself somewhere between (almost) any language such as C, C++, Rust and an ever growing list of languages, and your computer architecture (x86, ARM, etc).

This video by Mozilla Hacks can give you a better overview of WebAssembly and what it aims to achieve:

Enter AssemblyScript

Although still in the early stages - and considered experimental - AssemblyScript allows us to code up something we'd like to give a real kick of absinthe-like optimisation and compile it to WebAssembly. Now, we have all come to love TypeScript for its static typing crutch that gives some sanity to our JavaScript. We'll now be able to use TypeScript to compile .wasm binaries with the help of AssemblyScript.

First of all, you're going to want to initialise a new Node Project:

$  mkdir hello-wasm && cd hello-wasm
$  npm init

Then follow and complete the prompts (defaults are okay) to generate your package.json.

Now you'll go ahead and grab and install AssemblyScript as a development dependency:

$  npm install --save-dev AssemblyScript/assemblyscript

Initialise the AssemblyScript project with:

npx asinit .

You should now notice that your project structure looks like:

Structure of project after running npx asinit .
  • assembly/index.ts
    The entry file that will be compiled to WebAssembly. This is where we will do most of our coding.
  • build
    This directory will store the compiled WebAssembly artifacts.
  • index.js
    This is where the loading of the WebAssembly module and exporting its exports happens. Don't worry, we won't be writing much vanilla JS in this experiment 😉.

Let's take a look inside index.ts.

// The entry file of your WebAssembly module.

export function add(a: i32, b: i32): i32 {
  return a + b;
}

In the snippet above, the (boring) add() function is defined and just returns the sum of two integer inputs.

Going over to the index.js file in the root of the project:

const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const imports = {};
Object.defineProperty(module, "exports", {
  get: () => new WebAssembly.Instance(compiled, imports).exports
});

The output optimized.wasm binary is loaded as a WebAssembly module and its exports are exposed to us.

Okay, but how do we compile the optimized.wasm?

If you look back in package.json, you'll see that a run script was added for you during npx asinit .

...
"scripts": {
    ...
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
    "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
    "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized"
  },
...

npm run asbuild will generate an untouched.wasm (unoptimised) and an optimized.wasm binary.

After this build process, your build directory should look like this:

Build directory after npm run asbuild.

Now the .wasm files are the compiled WebAssembly and the .wat files are the WebAssembly text format that generated the .wasm files.

Let's take a closer look at untouched.wat:

(module
 (type $iii (func (param i32 i32) (result i32)))
 (type $v (func))
 (memory $0 0)
 (table $0 1 anyfunc)
 (elem (i32.const 0) $null)
 (global $HEAP_BASE i32 (i32.const 8))
 (export "memory" (memory $0))
 (export "table" (table $0))
 (export "add" (func $assembly/index/add))
 (func $assembly/index/add (; 0 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  get_local $0
  get_local $1
  i32.add
 )
 (func $null (; 1 ;) (type $v)
 )
)

And optimized.wat:

(module
 (type $iii (func (param i32 i32) (result i32)))
 (type $v (func))
 (memory $0 0)
 (table $0 1 anyfunc)
 (elem (i32.const 0) $null)
 (export "memory" (memory $0))
 (export "table" (table $0))
 (export "add" (func $assembly/index/add))
 (func $assembly/index/add (; 0 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  get_local $0
  get_local $1
  i32.add
 )
 (func $null (; 1 ;) (type $v)
  nop
 )
)

In this case, the only difference is the omission of nop in optimized.wat.

If you want to learn more about the .wat syntax, head over to the MDN page on Understanding WebAssembly text format.

Running our WebAssembly function

All we have to do to test out our add() function is to head back to the index.js entry file and add

...

const helloWasm = module.exports
console.log(helloWasm.add(4, 5));

Now run it with node index.js.

Congrats on running your first compiled WebAssembly!

Keep up to date with future #WebAssembly posts to start trying out some more exciting applications of this exciting new web standard!