Frontend Development 20 min read

An Introduction to WebAssembly: History, Advantages, and a Mandelbrot Demo

This article introduces WebAssembly, tracing its evolution from asm.js and NaCl to the modern binary format, outlines its performance, portability, and security benefits, and demonstrates its practical use by building a Mandelbrot set application with AssemblyScript and WebAssembly in the browser.

ByteFE
ByteFE
ByteFE
An Introduction to WebAssembly: History, Advantages, and a Mandelbrot Demo

1. Introduction

WebAssembly is a W3C standard that provides a portable, small‑size, fast‑loading binary format compatible with the web, enabling non‑JavaScript code to run efficiently in browsers and increasingly on servers and cloud platforms.

This article first reviews the history of WebAssembly, then summarizes its mission and advantages, and finally presents a simple WebAssembly browser application to give readers a hands‑on experience.

2. Evolution of WebAssembly

The technology originated from the need to overcome JavaScript performance limitations as web applications grew more complex. Three optimization stages emerged: asm.js, NaCl/PNaCl, and finally WebAssembly.

2.1 asm.js Phase

asm.js is a strict subset of JavaScript that adds static type annotations, allowing the engine to perform ahead‑of‑time optimizations and reuse compiled code, dramatically improving performance for numeric‑intensive workloads.

Example of asm.js type annotation using the bitwise‑or operator:

function fast_fib_module(stdlib, foreign, heap) {
    "use asm";
    function fib(n) {
        n = n|0;
        if (n >>> 0 < 3) {
            return 1|0;
        }
        return (fib((n-1)|0) + fib((n-2)|0))|0;
    }
    return fib;
}

asm.js suffered from limited browser support and other drawbacks, prompting the next stage.

2.2 NaCl and PNaCl Phase

Google Native Client (NaCl) allowed native C/C++ code to run in Chrome via architecture‑specific ELF binaries (.nexe). PNaCl introduced an architecture‑agnostic intermediate binary (.pexe) that could be distributed freely and compiled JIT‑style to the target architecture at runtime. However, both were Chrome‑only and added development complexity.

2.3 WebAssembly Phase

Building on asm.js, Mozilla released WebAssembly in 2015 as a binary format that can be loaded like a JavaScript module, offering fast execution, cross‑platform portability, and sandboxed security. By 2017 the major browsers converged on a MVP, and WebAssembly 1.0 became a W3C standard in 2019. Subsequent developments (e.g., WebAssembly 2.0) added reference types, SIMD, and bulk memory operations.

3. Goals and Advantages of WebAssembly

Performance: Statically typed, low‑level code runs near native speed with fast cold‑start and small binary size.

Portability: Language‑agnostic binary runs on browsers, back‑ends, mobile, IoT, and other platforms.

Sandboxed Execution: Isolated environment protects data and limits side‑channel attacks.

Standardization: Defined by the W3C WebAssembly Working Group ensures cross‑vendor compatibility.

Security: Runs under the same same‑origin and permission policies as other web code.

Flexible Development Model: Supports many source languages (C/C++, Rust, TypeScript, AssemblyScript, etc.) compiled to a common intermediate format.

4. WebAssembly Web Application Experience

To demonstrate WebAssembly’s benefits, a Mandelbrot set visualizer is built using AssemblyScript, compiled to a .wasm module, and integrated into an HTML page.

Step 1: Project Setup

npm init
npm install --save-dev assemblyscript

Step 2: Compile AssemblyScript to WebAssembly

npx asc Mandelbrot.ts --target release -o Mandelbrot.wasm

Step 3: AssemblyScript Source (Mandelbrot.ts)

// file: Mandelbrot.ts
/** Number of discrete color values on the JS side. */
const NUM_COLORS = 2048;
/** Computes a single line in the rectangle `width` x `height`. */
export function computeLine(y: u32, width: u32, height: u32, limit: u32): void {
    var translateX = width * (1.0 / 1.6);
    var translateY = height * (1.0 / 2.0);
    var scale = 10.0 / min(3 * width, 4 * height);
    var imaginary = (y - translateY) * scale;
    var realOffset = translateX * scale;
    var stride = (y * width) << 1;
    var invLimit = 1.0 / limit;
    var minIterations = min(8, limit);
    for (let x: u32 = 0; x < width; ++x) {
        let real = x * scale - realOffset;
        let ix = 0.0, iy = 0.0, ixSq: f64, iySq: f64;
        let iteration: u32 = 0;
        while ((ixSq = ix * ix) + (iySq = iy * iy) <= 4.0) {
            iy = 2.0 * ix * iy + imaginary;
            ix = ixSq - iySq + real;
            if (iteration >= limit) break;
            ++iteration;
        }
        while (iteration < minIterations) {
            let ixNew = ix * ix - iy * iy + real;
            iy = 2.0 * ix * iy + imaginary;
            ix = ixNew;
            ++iteration;
        }
        let col = NUM_COLORS - 1;
        let sqd = ix * ix + iy * iy;
        if (sqd > 1.0) {
            let frac = Math.log2(0.5 * Math.log(sqd));
            col =
((NUM_COLORS - 1) * clamp
((iteration + 1 - frac) * invLimit, 0.0, 1.0));
        }
        store
(stride + (x << 1), col);
    }
}
@inline
function clamp
(value: T, minValue: T, maxValue: T): T {
    return min(max(value, minValue), maxValue);
}

Step 4: Load and Run in the Browser (index.html)

// file: index.html
var cnv = document.getElementsByTagName("canvas")[0];
var ctx = cnv.getContext("2d");
var width = cnv.width;
var height = cnv.height;
const memory = new WebAssembly.Memory({ initial: ((byteSize + 0xffff) & ~0xffff) >>> 16 });
const mem = new Uint16Array(memory.buffer);
const imageData = ctx.createImageData(width, height);
const argb = new Uint32Array(imageData.data.buffer);
fetch("build/Mandelbrot.wasm")
  .then(r => r.arrayBuffer())
  .then(buffer => WebAssembly.instantiate(buffer, { env: { memory, "Math.log": Math.log, "Math.log2": Math.log2 } }))
  .then(module => {
    const { computeLine } = module.instance.exports;
    const limit = 40;
    for (let y = 0; y < height; ++y) {
      computeLine(y, width, height, limit);
      // map computed colors to RGBA buffer (omitted for brevity)
    }
    ctx.putImageData(imageData, 0, 0);
  })
  .catch(err => console.error(err));

Step 5: Serve the Application

cd $webassembly_tech/samples/mandelbrot
npx serve

The resulting page displays a colorful Mandelbrot fractal rendered efficiently thanks to WebAssembly.

5. Conclusion

The article has covered the full story of WebAssembly—from its origins in asm.js and NaCl to its modern role as a portable, high‑performance, sandboxed binary format—illustrated by a concrete Mandelbrot example, and hints at broader future applications.

6. References

[1] asm.js Working Draft: http://asmjs.org/spec/latest/ [2] Native Client (NaCl & PNaCl): https://www.chromium.org/nativeclient/ [3] WebAssembly Specification: https://webassembly.org/specs/ [4] Bringing WebAssembly outside the web with WASI: https://www.youtube.com/watch?v=fh9WXPu0hw8 [5] Mandelbrot set: https://en.wikipedia.org/wiki/Mandelbrot_set [6] Local Web Server: https://github.com/yaozhongxiao/cli/tree/master/server [7] webassembly_tech repository: https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples [8] Mandelbrot set (again): https://en.wikipedia.org/wiki/Mandelbrot_set [9] AssemblyScript: https://www.assemblyscript.org/ [10] Sample source: https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/mandelbrot/index.html [11] README for the sample: https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/mandelbrot/README.md

Performancefrontend developmentWebAssemblybrowserAssemblyScriptMandelbrot
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.