When WASM Is Not Worth the Complexity
When WASM Is Not Worth the Complexity
The Experiment
The e-commerce platform’s price calculation engine applies complex discount rules: tiered pricing, volume discounts, bundle deals, loyalty multipliers, and tax jurisdiction computation. The TypeScript implementation takes 45ms on a 4x throttled CPU for a cart with 50 items and 12 active discount rules. The team hypothesized that a WASM implementation would reduce this to under 15ms.
They were wrong.
The Benchmark
The WASM price calculator was implemented in Rust, compiled with wasm-pack, and integrated using the same bindings pattern from Chapter 6.
| Operation | TypeScript | WASM | WASM Overhead |
|---|---|---|---|
| Data marshaling (cart to WASM memory) | 0ms | 8ms | +8ms |
| Discount rule evaluation | 42ms | 12ms | -30ms |
| Data marshaling (result to JS) | 0ms | 3ms | +3ms |
| Total | 42ms | 23ms | -19ms |
The WASM computation is 3.5x faster than TypeScript (12ms vs 42ms). But the data marshaling overhead of 11ms eats 37% of the raw speedup. The net improvement is 19ms.
19ms is below the perception threshold. On the checkout page, the price recalculation happens after the user applies a coupon or changes a quantity. The 42ms TypeScript implementation is already below the 50ms long task threshold. The user does not perceive a 42ms delay as lag.
The WASM implementation cost:
- 3 days of Rust development
- 142KB additional WASM binary (68KB gzipped)
- Ongoing maintenance of the Rust codebase alongside TypeScript
- Build pipeline complexity (Rust toolchain in CI)
The benefit: 19ms improvement on an operation that was already imperceptible.
Why V8 Is Fast Enough
V8’s optimizing compiler (TurboFan) produces highly efficient machine code for predictable TypeScript patterns. The price calculation function has:
- Typed numeric operations (all
numberarithmetic) - Predictable control flow (for loops over arrays)
- Monomorphic function calls (same types on every invocation)
- No polymorphism, no dynamic property access, no
eval
These characteristics allow TurboFan to generate machine code that approaches WASM performance. The JIT compilation overhead is paid once and amortized across invocations.
WASM has a consistent advantage for:
- SIMD operations: WASM SIMD processes 4 float32 values per instruction. JavaScript has no SIMD equivalent. Image processing, audio processing, and matrix math benefit.
- Predictable performance: WASM has no JIT warmup. The first invocation is as fast as the hundredth. TypeScript’s first invocation runs in the interpreter, which is 10-100x slower than optimized code.
- 64-bit integer arithmetic: JavaScript’s
numbertype is a 64-bit float. Integer operations that require 64-bit precision (cryptography, hash computation) must useBigInt, which is significantly slower than WASM’s native i64. - Memory-intensive computation: WASM’s linear memory allows dense, cache-friendly data structures. JavaScript objects have header overhead and are scattered across the heap.
For the price calculator, none of these advantages apply. The computation uses standard floating-point arithmetic, runs more than once per session (JIT warmup is amortized), and operates on a small dataset that fits in L1 cache regardless of memory layout.
The Decision Framework
interface WasmDecision {
typescriptDuration: number; // ms on 4x throttled CPU
expectedWasmDuration: number; // ms (benchmark or estimate)
marshalingOverhead: number; // ms for data transfer
isUserFacing: boolean; // Does it affect INP?
invocationFrequency: "once" | "repeated" | "per-frame";
}
function shouldUseWasm(decision: WasmDecision): string {
const netImprovement =
decision.typescriptDuration -
(decision.expectedWasmDuration + decision.marshalingOverhead);
if (decision.typescriptDuration < 50) {
return (
"NO: TypeScript is already under the 50ms long task threshold. " +
"WASM complexity is not justified."
);
}
if (netImprovement < 30) {
return (
"NO: Net improvement after marshaling is under 30ms. " +
"The user will not perceive the difference."
);
}
if (!decision.isUserFacing) {
return (
"MAYBE: The operation does not affect INP. " +
"Move it to a Web Worker instead. WASM in a Worker is " +
"justified only if TypeScript in a Worker is still too slow."
);
}
if (decision.invocationFrequency === "per-frame") {
return (
"YES: Per-frame operations benefit from WASM's predictable " +
"performance and lack of GC pauses."
);
}
if (netImprovement > 100) {
return (
"YES: Over 100ms net improvement on a user-facing operation " +
"justifies the complexity cost."
);
}
return (
"MAYBE: 30-100ms improvement. Evaluate whether a Web Worker " +
"alone provides sufficient improvement."
);
}
Applying this framework to the e-commerce platform’s candidates:
| Operation | TS (ms) | WASM (ms) | Marshal (ms) | Net (ms) | Verdict |
|---|---|---|---|---|---|
| Image conversion | 280 | 85 | 8 | 187 | YES |
| Price calculation | 42 | 12 | 11 | 19 | NO |
| Search ranking | 120 | 35 | 15 | 70 | MAYBE* |
| Cart validation | 8 | 3 | 6 | -1 | NO |
*Search ranking was moved to a Web Worker (Chapter 5) instead, which reduced INP from 180ms to 12ms without WASM. The 120ms computation time in a Worker does not affect INP because it runs off the main thread. WASM would reduce the result latency from 120ms to 50ms, but users did not report the 120ms delay as problematic.
The cart validation example demonstrates the worst case for WASM: the marshaling overhead (6ms) exceeds the computation saving (5ms), making WASM slower than TypeScript.
The Proof
The team removed the WASM price calculator after one sprint and reverted to the TypeScript implementation. The results:
- INP unchanged (42ms was already below threshold)
- WASM binary removed from deployment (68KB saved)
- CI build time reduced by 45 seconds (Rust compilation step removed)
- One less toolchain for the team to maintain
The image conversion WASM module was kept because it met the criteria: 187ms net improvement on a user-facing operation with per-session frequency.
The Trade-off
This framework is conservative. It biases toward not using WASM, which means some operations that would benefit from WASM are left in TypeScript. The cost of this conservatism is a slower application for edge-case workloads (very large carts, very complex discount rules). The benefit is a simpler codebase that the entire team can maintain.
For teams with Rust or C++ expertise already on the roster, the complexity cost of WASM is lower, and the threshold for “worth it” shifts downward. For teams that would need to hire or train for WASM development, the threshold is higher. The millisecond numbers in the decision framework are calibrated for a team where WASM is an additional skill, not an existing one.