$ benchmark

Cryptographic performance analysis - JS vs WebCrypto vs WASM

target: browseruser: amorfatimode: benchmark

Controls

Results

Technical Breakdown

1. Pure JavaScript (ChaCha20 Polyfill)

JavaScript runs on a single thread with an event loop. Heavy CPU operations like ChaCha20 quarter rounds block the main thread entirely. Each byte goes through add-rotate-xor cycles in JS number space (32-bit integers coerced from doubles). The JS engine cannot optimize across async boundaries, so sustained throughput is limited by the CPU clock rate multiplied by instruction count per byte. Expect single-digit MiB/s - the bottleneck is raw JS execution speed for cryptographic primitives.

2. WebCrypto API (AES-GCM)

crypto.subtle.encrypt is backed by native C++, but every call returns a Promise. Each 16 KB record incurs:

  • A full micro-task queue drain before resolve (typically 0.05-0.2 ms overhead per call)
  • Buffer serialization across the JS → C++ boundary
  • GCM-authenticated encryption, which includes GHASH polynomial evaluation

While each individual encrypt is fast (~0.01 ms for 16 KB on modern hardware), the aggregate throughput tops out in the low-to-mid tens of MiB/s due to per-call promise scheduling overhead and the serialized record-by-record processing model imposed by TLS semantics.

3. WebAssembly (XOR Cipher)

WASM executes in a separate linear memory space with synchronous function calls. The XOR cipher:

  • Bypasses the JS event loop entirely - no Promises, no micro-task queue
  • Runs in a sandboxed VM with ahead-of-time compilation to native code (Liftoff/Wizard-tier on V8)
  • Operates on raw linear memory, avoiding JS → native serialization overhead

Because WASM calls are synchronous, a large buffer can be processed in a single call without yielding. The main thread is still blocked (cooperative scheduling), but the throughput per call is significantly higher since there is zero promise overhead per record. Expect 2-5x improvement over the JS polyfill and measurably lower event loop lag than the Promise-based WebCrypto approach at equivalent payload sizes.

Key Insight

The benchmark demonstrates that while WebCrypto uses native code, the per-record Promise dispatch creates a micro-task queue tax that limits throughput for high-frequency TLS-like workloads. WASM offers a middle ground: near-native execution with synchronous semantics, at the cost of manual memory management and the WASM sandbox boundary. Pure JS remains the slowest option due to interpreted arithmetic and heap allocation pressure.