Why We Chose Rust and WebAssembly for Engineering Calculation Tools
A technical look at why ChainSolve's computation engine is written in Rust and compiled to WebAssembly, and what this means for performance, reliability, and portability.
The Requirements
When we set out to build ChainSolve’s calculation engine, we had a clear set of requirements:
-
Near-native computation speed. Engineering calculations can involve large matrices, iterative solvers, and complex dependency graphs with hundreds of nodes. The engine must evaluate these in milliseconds, not seconds.
-
Browser execution. The tool must work entirely in the browser with no server round-trips for computation. Engineers working on sensitive projects cannot send proprietary calculation data to external servers.
-
Offline capability. Engineers in test facilities, on factory floors, and at customer sites often have limited or no internet connectivity. The tool must work offline once loaded.
-
Memory safety. A calculation tool that crashes or produces silently wrong results due to memory corruption is worse than useless. Correctness is non-negotiable.
-
Portability. The same engine must run in browsers, in Node.js for CI/CD integration, and potentially as a native CLI tool.
These requirements narrowed the field considerably.
Why Not JavaScript?
JavaScript is the obvious choice for browser-based applications. It is the native language of the web, has excellent tooling, and a massive ecosystem. For UI code, we use JavaScript (specifically TypeScript with React). But for the computation engine, JavaScript has fundamental limitations.
JavaScript is a dynamically typed, garbage-collected language. For UI rendering and event handling, this is fine. For numerical computation, it introduces two problems:
Performance ceiling. Modern JavaScript engines like V8 are remarkably fast for a dynamic language, but they cannot match the performance of ahead-of-time compiled code for numerically intensive work. JIT compilation adds unpredictable pauses. Garbage collection adds latency spikes. These are acceptable in a UI layer but problematic in a computation engine that needs consistent, fast evaluation.
Numerical precision. JavaScript has a single number type: IEEE 754 double-precision floating point. This is adequate for most engineering calculations, but JavaScript provides no way to control floating-point behaviour, no SIMD intrinsics for vectorised computation, and no integer types for cases where exact integer arithmetic is required (such as graph algorithms on the dependency graph).
A JavaScript calculation engine would work. It would be adequate for small to medium calculations. But it would hit a performance ceiling with complex chains, and it would require careful workarounds for numerical edge cases.
Why Rust?
Rust satisfies every one of our computation requirements:
Performance. Rust compiles to machine code (or WebAssembly) with no garbage collector and no runtime overhead. Numerical code in Rust performs comparably to C and C++. For ChainSolve’s calculation engine, this means chain evaluation is consistently fast, with no GC pauses or JIT warmup.
Memory safety without garbage collection. Rust’s ownership system guarantees memory safety at compile time. There are no null pointer dereferences, no use-after-free bugs, no data races. For a calculation tool where correctness is paramount, this is a significant advantage over C and C++.
WebAssembly target. Rust has first-class support for compiling to WebAssembly via wasm-pack and wasm-bindgen. The resulting WASM module runs in any modern browser at near-native speed. This gives us browser execution and offline capability without sacrificing performance.
Strong type system. Rust’s type system lets us encode engineering constraints in the type system itself. A UnitValue<Millimeters> cannot be accidentally added to a UnitValue<Inches>, the compiler rejects it. This catches entire categories of engineering errors at compile time.
Portability. The same Rust code compiles to WASM for browser execution, to native binaries for CLI tools, and to shared libraries for integration with other tools. One codebase, multiple targets.
The Architecture
ChainSolve’s architecture separates computation from presentation:
+------------------------------------------+
| Browser (TypeScript + React) |
| - UI components |
| - Chain editor |
| - Visualisation |
+------------------------------------------+
| wasm-bindgen interface |
+------------------------------------------+
| WASM Module (Rust) |
| - Dependency graph solver |
| - Block evaluation engine |
| - Unit conversion system |
| - Expression parser |
| - Numerical methods library |
+------------------------------------------+
The Rust/WASM module exposes a clean API to the TypeScript layer via wasm-bindgen. The TypeScript layer handles all UI concerns, rendering, user interaction, layout. The Rust layer handles all computation, graph traversal, block evaluation, unit conversion, expression parsing.
This separation means the UI never blocks on computation. Chain evaluation happens in the WASM module, returns results to TypeScript, and the UI updates. For very large chains, we run the WASM module in a Web Worker to keep the UI thread completely responsive.
Performance Results
Some representative benchmarks from our development builds (measured on a mid-range laptop, M2 MacBook Air):
| Operation | JavaScript (baseline) | Rust/WASM |
|---|---|---|
| Evaluate 100-block chain | 12 ms | 0.8 ms |
| Evaluate 1,000-block chain | 340 ms | 8 ms |
| Topological sort (1,000 nodes) | 5 ms | 0.3 ms |
| Unit conversion (10,000 values) | 18 ms | 0.6 ms |
| Expression parse + evaluate | 0.4 ms | 0.02 ms |
The Rust/WASM implementation is consistently 15-40x faster than the equivalent JavaScript. For small chains, both are fast enough. For large chains (which production users will inevitably create), the difference is between instant feedback and perceptible delay.
Challenges and Trade-offs
Rust and WebAssembly are not without trade-offs:
Learning curve. Rust is a more complex language than JavaScript or Python. The ownership system, while powerful, requires a mental model that takes time to develop. For a solo founder, this was an investment in long-term productivity at the cost of short-term velocity.
WASM binary size. The compiled WASM module is currently approximately 800 KB gzipped. This is a meaningful addition to the initial page load. We mitigate this with lazy loading, the WASM module is loaded asynchronously after the UI shell renders.
Debugging. Debugging Rust code compiled to WASM is improving but still less convenient than debugging JavaScript in browser DevTools. We rely heavily on Rust’s test suite (run natively) and use WASM-specific logging for browser debugging.
Ecosystem. The Rust ecosystem for numerical computation is growing but less mature than Python’s NumPy/SciPy ecosystem. We have implemented some numerical methods from scratch where existing Rust crates were insufficient.
Despite these trade-offs, the decision to use Rust and WebAssembly has been validated by the performance and reliability characteristics of the resulting engine. For a tool where engineers will rely on the results for safety-critical decisions, the compile-time guarantees that Rust provides are worth the additional development complexity.
Conclusion
Choosing Rust and WebAssembly for ChainSolve’s calculation engine was a deliberate architectural decision driven by engineering requirements, not technology fashion. Near-native performance in the browser, memory safety without garbage collection, and a strong type system that catches unit errors at compile time, these are not nice-to-haves for an engineering calculation tool. They are requirements.
If you are interested in the technical details of our Rust/WASM architecture, or if you are evaluating similar technology choices for your own engineering tools, we welcome the conversation. Reach out via our contact page.