How WebAssembly Boosts Web Performance: From asm.js to Near‑Native Speed
This article explains the evolution from asm.js to WebAssembly, compares their performance, outlines implementation strategies—including AssemblyScript, hand‑written asm.js, and C/C++ compilation with Emscripten—and provides practical code examples and tips for integrating WebAssembly into web applications.
What is WebAssembly
asm.js
Before understanding WebAssembly we need to know asm.js:
asm.js is a JavaScript‑based syntax standard proposed by Mozilla to improve execution efficiency. It is not a new language but a subset of JavaScript, so any asm.js program is valid JavaScript and can run in browsers that do not support asm.js.
asm.js can be seen as a statically‑typed form of JavaScript, so some browsers (e.g., Firefox) use a separate ahead‑of‑time (AOT) compiler to translate asm.js code into machine instructions. Type errors cause the code to fall back to normal JavaScript execution. The static typing and AOT give asm.js a large performance boost over plain JS.
Data source: http://ejohn.org/blog/asmjs-javascript-compile-target/
WebAssembly
Although asm.js performs well, it has drawbacks: text‑based parsing is slow and code size is large. WebAssembly solves these by providing a binary format.
WebAssembly is a new low‑level code that runs in modern browsers. It uses a compact binary format, delivers near‑native performance, and serves as a compilation target for languages such as C/C++. It is designed to work alongside JavaScript.
In March 2017 the four major browsers (Chrome, Safari, Edge, Firefox) shipped a WebAssembly MVP that matches asm.js functionality.
What WebAssembly Can Do
The current MVP is similar to a WebWorker: it can perform numeric calculations but cannot directly manipulate the DOM. Future road‑maps plan DOM access. At present, WebAssembly is best suited for intensive data‑processing tasks rather than business logic.
For example, a map‑rendering algorithm that processes grid lines can run three times faster with WebAssembly, dramatically speeding up map generation. The open‑source earcut library for triangulation can also be ported to WebAssembly for faster results.
Implementation Options
Our goal is to produce a codebase that can compile to both WebAssembly and asm.js, using asm.js as a fallback for browsers that lack full WebAssembly support.
Option 1: AssemblyScript
AssemblyScript is an open‑source project that defines a TypeScript subset that compiles to WebAssembly. It is friendly to front‑end developers with TypeScript experience, but its asm.js output is still experimental and unstable, so we do not adopt it.
Option 2: Hand‑written asm.js
Writing asm.js manually and then compiling with Binaryen to WebAssembly is feasible. Important considerations include:
1. Types – asm.js variables are int, float, or double; arrays and strings are not supported.
2. Syntax – type annotations use bitwise operators, e.g.:
<code>// Mark function f1 return value as int32
intValue = f1()|0;
// Mark function f2 return value as double
doubleValue = +f2();
// Mark function f3 return value as float
floatValue = Math.fround(f3());
</code>3. Memory – asm.js uses an ArrayBuffer created in JavaScript; its size must be a multiple of a page (64 KB). Arrays can be simulated via memory offsets.
The following asm.js module generates an arbitrary‑length array and exposes start/end offsets for JavaScript to read:
<code>/**
* @param {Window} stdlib, window object
* @param {Object} foreign, optional imports
* @param {ArrayBuffer} buffer, memory
*/
function MyAsmModule(stdlib, foreign, buffer) {
"use asm";
var heap8 = new stdlib.Int8Array(buffer);
var START = 0;
var END = 0;
function getStart() { return START|0; }
function getEnd() { return END|0; }
function generateArray(count) {
count = count|0;
var i = 0;
for (i = 0; (i|0) < (count|0); i = (i+1)|0) {
heap8[i>>0] = i|0;
}
END = i;
}
return {generateArray: generateArray, getStart: getStart, getEnd: getEnd};
}
// JS usage
var memory = new ArrayBuffer(0x10000);
var module = MyAsmModule(window, {}, memory);
module.generateArray(100);
var generatedArray = new Int8Array(memory).slice(module.getStart(), module.getEnd());
</code>Hand‑written asm.js is cumbersome, hard to maintain, and requires careful memory alignment when compiling to wasm, so we do not recommend this path.
Option 3: C/C++ Compilation
The mainstream approach uses Emscripten to compile C/C++ to asm.js or wasm. Two issues were encountered:
In ONLY_MY_CODE mode the generated asm.js is not valid asm.js. The recommended workaround is to use SIDE_MODULE instead.
Emscripten requires a minimum memory allocation of 16 MB for wasm, which is wasteful for small modules.
We solved these by compiling C++ to asm.js, then using Binaryen to convert asm.js to the textual .wat format, and finally using wabt to produce a binary wasm file. This pipeline yields both asm.js and wasm from a single codebase.
Reference Example
The following C code implements the same array‑generation module:
<code>// Allocate memory: 0x1000 * 4 = 16384 bytes
static int s_array[0x1000];
static int s_current_index = 0;
int* get_start() { return s_array; }
int* get_end() { return &s_array[s_current_index]; }
void generate_array(int count) {
for (int i = 0; i < count; ++i) {
s_array[i] = i;
}
s_current_index = count;
}
</code>JavaScript can load the compiled wasm and call the exported functions:
<code>// Fetch the wasm module
fetch('./example.wasm')
.then(r => r.arrayBuffer())
.then(bytes => {
var memory = new WebAssembly.Memory({initial:1, maximum:1});
return WebAssembly.instantiate(bytes, {
env: {memory: memory, table: new WebAssembly.Table({initial:0, maximum:0, element:'anyfunc'})},
global: {NaN: NaN, Infinity: Infinity}
});
})
.then(results => {
var module = results.instance.exports;
module._generate_array(100);
var generatedArray = new Int32Array(memory.buffer)
.slice(module._get_start()>>2, module._get_end()>>2);
console.log(generatedArray);
});
</code>Feel free to try this approach in a browser.
Practical Tips
Carefully evaluate memory requirements; insufficient allocation causes runtime errors.
Wasm loading depends on the network; you can embed the binary as an ArrayBuffer to avoid extra requests, at the cost of doubling script size.
Compilation takes time; consider compiling during idle periods to avoid UI jank.
Benefits
Using WebAssembly for map‑engine data processing increased parsing speed by roughly threefold, reducing average grid‑parsing time by 25% after deployment. As more users upgrade to browsers that support WebAssembly, performance will continue to improve.
Because WebAssembly excels at data‑intensive computation, some sites embed mining code in pages, causing high CPU usage. Our code does not perform mining.
References
http://webassembly.org/docs/mvp/ https://developer.mozilla.org/zh-CN/docs/WebAssembly https://software.intel.com/zh-cn/articles/html5-asmjs http://www.infoq.com/cn/news/2017/07/WebAssembly-solve-JavaScript
Baidu Maps Tech Team
Want to see the Baidu Maps team's technical insights, learn how top engineers tackle tough problems, or join the team? Follow the Baidu Maps Tech Team to get the answers you need.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.