shtabnoy.com / challenges / wasm
✦ Browser internals

C → WebAssembly
in the browser

Write functions in C, compile them to WASM, and race them against JavaScript. Same algorithms, same browser — see where compiled C pulls ahead and where V8's JIT keeps up.

WebAssemblyCEmscriptenDockerbenchmarking
// the pipeline

From C source to browser execution

C code compiled to a .wasm binary — a compact, pre-compiled format the browser executes at near-native speed.

math.c
C source
emcc
Emscripten
.wasm
binary
Browser
WebAssembly API
Minimal compile (just .wasm)
# Minimal — produces just math.wasm
docker run --rm -v $(pwd):/src emscripten/emsdk \
  emcc math.c -o math.wasm --no-entry \
  -s EXPORTED_FUNCTIONS='["_fibonacci","_hash_djb2",\
    "_count_primes"]' -O2
Emscripten compile (.js + .wasm)
# Full — produces math_full.js + math_full.wasm
docker run --rm -v $(pwd):/src emscripten/emsdk \
  emcc math.c -o math_full.js \
  -s EXPORTED_FUNCTIONS='["_fibonacci","_hash_djb2",\
    "_count_primes"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall"]' \
  -s MODULARIZE=1 -O2
// loading

Two ways to load WASM

The raw browser API is 3 lines. Emscripten adds a JS loader for convenience. Both run the same compiled binary — try them both.

Minimal — raw WebAssembly API
// Raw browser API — no libraries, no glue code

const response = await fetch('/wasm/math.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);

// Call C functions directly — just numbers in, numbers out
instance.exports.fibonacci(38);
instance.exports.hash_djb2(123456);
instance.exports.count_primes(100000);
Emscripten — ccall convenience
// Emscripten glue — convenience for complex programs

const Module = await loadEmscriptenModule('/wasm/math_full.js');

// ccall: function name, return type, arg types, args
Module.ccall('fibonacci', 'number', ['number'], [38]);
Module.ccall('hash_djb2', 'number', ['number'], [123456]);
Module.ccall('count_primes', 'number', ['number'], [100000]);
💡 When to use which

Minimal for pure number crunching — smaller, no dependencies. Emscripten when your C code needs strings, arrays, file I/O, or malloc — the glue emulates all the system interfaces C expects.

// benchmark

WASM vs JavaScript — same algorithm, same browser

Three algorithms implemented identically in C and JavaScript. Fibonacci tests simple recursion. Hashing tests bitwise ops in tight loops. Prime counting tests sustained nested integer computation — all inside a single WASM call.

// the code

The C source

Pure computation. No #include, no main(), no I/O. Just functions that take numbers and return numbers.

math.c
// Pure computation — no I/O, no malloc, just numbers.

int fibonacci(int n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

unsigned int hash_djb2(unsigned int input) {
  unsigned int hash = 5381;
  while (input > 0) {
    unsigned int digit = input % 10;
    hash = ((hash << 5) + hash) + digit;
    input /= 10;
  }
  return hash;
}

// Nested loops, all computation inside WASM
int count_primes(int max_n) {
  int count = 0;
  for (int n = 2; n <= max_n; n++) {
    int is_prime = 1;
    for (int i = 2; i * i <= n; i++) {
      if (n % i == 0) { is_prime = 0; break; }
    }
    count += is_prime;
  }
  return count;
}
// takeaways

What this exercise teaches

WASM wins on sustained computation
Bitwise hashing and prime counting show clear speedups. WASM's pre-compiled code has no JIT warmup and no type uncertainty — it runs fast from the first instruction. The advantage grows with computation complexity.
🤝
V8 is remarkably good
For simple integer recursion like fibonacci, V8's JIT compiler produces nearly identical machine code. WASM isn't a blanket "faster than JS" — it depends on the workload.
🔌
JS is still the glue
WASM can't touch the DOM, make network requests, or handle events. JavaScript orchestrates the browser; WASM accelerates the math. They're partners, not competitors.
🌍
Any language, same runtime
C, C++, Rust, Go, and more all compile to the same .wasm format. The browser doesn't know or care what language produced the binary — it's just bytes.