Comparing TypeScript Runtime Performance: Native JS vs. WebAssembly

Learn how to compare runtime performance of TypeScript code executed as native JavaScript versus using WebAssembly, with simple examples and benchmarks.

TypeScript is a popular language that compiles to JavaScript, allowing developers to write safer and more maintainable code. Typically, TypeScript code runs directly as JavaScript in browsers or Node.js environments. However, WebAssembly (Wasm) is another option for running code in web environments, designed for near-native performance. In this tutorial, we will compare the runtime performance of a simple algorithm implemented in TypeScript, running as native JavaScript, versus running as WebAssembly.

WebAssembly is a low-level bytecode that runs in modern browsers and enables faster execution for compute-heavy tasks. While it's commonly generated from languages like C, C++, or Rust, you can also compile TypeScript logic to WebAssembly indirectly through those languages or use WebAssembly modules alongside JavaScript.

Let's start with a simple example: calculating the nth Fibonacci number. We will write this algorithm in TypeScript, run it as native JS, then demonstrate how a WebAssembly version can be integrated for performance comparison.

typescript
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Test the function
console.log('Fibonacci(10):', fibonacci(10));

This function calculates the nth Fibonacci number recursively. It works well for small inputs but can be slow for larger numbers because of exponential time complexity.

To measure native JavaScript performance, we use the `console.time` and `console.timeEnd` methods to benchmark the function:

typescript
const n = 35;
console.time('Native JS Fibonacci');
const result = fibonacci(n);
console.timeEnd('Native JS Fibonacci');
console.log(`Result: ${result}`);

Next, let’s compare this with a simple WebAssembly module. Since WebAssembly is usually created from lower-level languages like C, we will assume you have a wasm file compiled from an equivalent C Fibonacci function. To keep this beginner-friendly, instead of writing the C code and compiling it here, we'll focus on how to load and call a WebAssembly module in TypeScript/JavaScript.

Here’s an example of loading a WebAssembly module and calling a Fibonacci function exported from it:

typescript
async function loadWasmAndRun(n: number) {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const wasmModule = await WebAssembly.instantiate(buffer);

  console.time('WebAssembly Fibonacci');
  const result = wasmModule.instance.exports.fibonacci(n);
  console.timeEnd('WebAssembly Fibonacci');
  console.log(`WASM Result: ${result}`);
}

loadWasmAndRun(35);

To create 'fibonacci.wasm', you would implement the Fibonacci function in C, compile it with `emcc` or another compiler to wasm, and serve the resulting .wasm file alongside your web app.

Once you run both the native JavaScript and WebAssembly benchmarks, you can compare the time they take. WebAssembly usually performs better for numeric computations and tight loops, especially when the logic is compiled from efficient languages like C or Rust.

Keep in mind, WebAssembly interop comes with some overhead for passing data between JS and Wasm. So, for small or simple tasks, native JavaScript might be just as fast or faster because it avoids this communication overhead.

In summary, if you are working on performance-critical parts of your TypeScript app, consider integrating WebAssembly for compute-heavy functions. For general application logic, native JavaScript from TypeScript compilers usually offers sufficient performance and better developer ergonomics.

We hope this comparison helps you understand where each approach shines and how to practically blend native JS and WebAssembly to optimize your applications.