JS 2 is a research preview of an open source compiler focused on compiling JavaScript to WebAssembly ahead of time, developed at Loopdive.


Mission: JavaScript, compiled.

This is an early-stage research prototype and technical demo, not a production-ready compiler. Expect rough edges, incomplete language and standard-library coverage, and breaking changes between versions.

On compatibility: today, compiled modules still require a JavaScript host and rely on the host's WebAssembly GC support to run. That is not a fixed design constraint — reducing the dependency on the JS host and on host-provided GC, so more code runs as standalone WebAssembly, is an explicit goal that is actively being worked on, and the requirement is expected to shrink over time.

The mission of the JS² compiler project is to make WebAssembly a drop-in deployment target for existing JavaScript and npm packages, running modules without embedding a JS runtime and enabling more secure, flexible dependency management.

The project is motivated by the idea that JavaScript, despite being a dynamic language, can be compiled efficiently ahead of time without limiting it to an incompatible subset or shipping a full JS engine inside every module. Whether that idea holds up at scale is an open question the project is testing.

The project is guided by the following goals:

JavaScript logo

JS Ecosystem Compatible

Compile existing code without rewriting it into a restricted subset, and stay compatible with the full JS ecosystem, the web, and NPM. Access Web APIs without writing glue code.

WebAssembly logo

No Embedded Runtime

Deploy compiled JavaScript modules in standalone WebAssembly runtimes such as Wasmtime. Code known at compile time is translated directly to WebAssembly — no embedded JS engine or interpreter — keeping modules small and fast. Only code generated at runtime ( eval / new Function ) falls back to a small embedded bytecode interpreter.

🔒

Sandbox Untrusted Code

Reduce supply chain attack surface by isolating JavaScript modules from each other inside the same application and keeping filesystem, network, DOM, and globals behind explicit imports.

🧩

Plug & Play

Dynamic module linking allows to make code more modular so consumers can choose implementations, control upgrades, and swap dependencies for each environment.


Goal: 100% ECMAScript compatibility.

Which JS language features compile to WebAssembly today, which still rely on host support, and which are still on the roadmap. Status is derived from ECMAScript Test262 pass rates and known compiler limitations.

Each edition's percentage and pass / total count cover every Test262 test in that edition. The feature rows below are illustrative examples — a hand-picked subset, not a complete breakdown — so their individual counts are not meant to sum to the edition total.

Compatibility Test Report

ECMAScript Conformance Pass Rate Over Time

Pass rate Tests passed Total tests run
ES3 / Core
Primitive types (string, number, boolean, null, undefined) All primitive value types and coercion
const s = "hello";
const n = 42;
const b = true;
const x = null;
(func $__module_init
  string.const "hello"
  global.set $s
  f64.const 42
  global.set $n
  i32.const 1           ;; true
  global.set $b)
Operators (arithmetic, comparison, logical, bitwise) All standard operators including ternary
const sum = a + b;
const eq = x === y;
const ok = a > 0 && b > 0;
const bits = n | 0xFF;
(func $example (param f64 f64)
    (result f64)
  local.get 0
  local.get 1
  f64.add               ;; a + b
)
;; x === y → f64.eq (or ref.eq)
;; n | 0xFF → i32.or
typeof / instanceof Runtime type checking operators
typeof x === "string";
obj instanceof Array;
;; typeof x === "string"
;; compiles to tag check on struct
local.get $x
ref.test (ref $String)
;; instanceof → ref.test
delete operator Remove object properties
const obj = { a: 1, b: 2 };
delete obj.b;
;; delete obj.b
local.get $obj
ref.null extern
struct.set $Obj $b
Comma operator Evaluate expressions left to right
const x = (1, 2, 3); // x === 3
f64.const 1
drop
f64.const 2
drop
f64.const 3
Labeled statements (break / continue) Named loop targets for break and continue
outer: for (let i = 0; i < 3; i++) {
  for (let j = 0; j < 3; j++) {
    if (j === 1) continue outer;
  }
}
(block $outer
  (loop $outer_loop
    (block $inner
      (loop $inner_loop
        ;; if j === 1
        br $outer_loop
      ))
    br $outer_loop
  ))
for-in Iterate over object property names
for (const key in obj) {
  console.log(key);
}
local.get $obj
call $Object.keys
local.set $keys
;; iterate keys array
arguments object (full) Legacy arguments — partial, rest params preferred
function legacy() {
  return arguments.length;
}
// prefer rest params: (...args)
Rest parameters (...args) are the preferred form and are fully compiled; the legacy full arguments object is the more limited path.
host eval() Dynamic code evaluation via JS host import
eval("1 + 2"); // delegated to host
Compiles to a host import that delegates to the JS engine. Requires a JS host runtime; not available in standalone Wasm.
with statement Dynamic scope extension
with (obj) { x; }
Disallowed in strict mode. All modules run strict.
ES5
Variables (var, let, const) Block-scoped and function-scoped variable declarations
let count = 1;
const name = "hello";
var legacy = true;
(func $__module_init
  (local $count f64)
  (local $name externref)
  (local $legacy i32)
  f64.const 1
  local.set $count
  string.const "hello"
  local.set $name
  i32.const 1
  local.set $legacy)
Functions & closures Named functions, expressions, and lexical closures
function greet(name) {
  return "Hi " + name;
}

const add = (a, b) => a + b;
(func $greet (param (ref null $str))
    (result (ref null $str))
  string.const "Hi "
  local.get 0         ;; name
  string.concat
  return)
Control flow Branching, loops, and switch statements
if (x > 0) {
  handle();
} else {
  fallback();
}

for (let i = 0; i < 10; i++) {
  process(i);
}
(func $test (param f64)
    (result f64)
  local.get 0
  f64.const 0
  f64.gt
  (if (result f64)
    (then local.get 0 return)
    (else local.get 0 f64.neg return)))
try / catch / finally Exception handling with optional finally block
try {
  riskyOp();
} catch (e) {
  handle(e);
} finally {
  cleanup();
}
(func $safe (result f64)
  (try
    (do call $riskyOp)
    (catch 0
      local.set $e
      f64.const -1
      return)))
throw Throw custom and built-in error objects
throw new Error("something went wrong");
(func $__module_init
  i32.const 1
  string.const "something went wrong"
  struct.new $Error
  throw 0)
Objects Literals, property access, methods, and shorthand syntax
const obj = {
  name: "js2wasm",
  version: 1,
  greet() { return this.name; }
};
(type $obj (struct
  (field $__tag i32)
  (field $name externref)
  (field $version f64)))

(func $obj_greet (param (ref null $obj))
    (result externref)
  local.get 0
  struct.get $obj $name
  return)
Strings String methods, concatenation, and manipulation
"hello".toUpperCase();   // "HELLO"
"a,b,c".split(",");
"hello".slice(1, 3);      // "el"
(func $__module_init
  ;; "hello".toUpperCase()
  string.const "hello"
  call $string.toUpperCase ;; host
  ;; "hello".slice(1, 3)
  string.const "hello"
  i32.const 1  i32.const 3
  string.substring)
Numbers Math operations, parseInt, Number methods
Math.max(1, 2);
parseInt("42");
(3.14).toFixed(1);
(func $__module_init
  (local $a f64) (local $b f64)
  f64.const 1
  local.set $a
  f64.const 2
  local.set $b
  local.get $a  local.get $b
  f64.max
  drop)
host JSON Parse and stringify JSON data
const obj = JSON.parse('{"a": 1}');
const str = JSON.stringify({ a: 1 });
(func $__module_init
  global.get 0
  call $JSON.parse
  global.set $obj)
Error types Error, TypeError, RangeError, SyntaxError and more
throw new TypeError("expected string");
throw new RangeError("out of bounds");
(func $__module_init
  i32.const 2
  string.const "expected string"
  struct.new $TypeError
  throw 0)
host Arrays Array methods and iteration helpers
const doubled = [1, 2,
  3].map(x => x * 2);
const sum = arr.reduce((a,
  b) => a + b, 0);
const found = arr.find(x => x > 5);
(func $__module_init
  ;; [1, 2, 3].map(x =&gt; x...
  f64.const 1  f64.const 2  f64.const 3
  array.new_fixed 3
  ref.func $callback
  call $__arr_map
Most built-in methods work. Some iterator edge cases and sparse array handling incomplete.
host Regular expressions Pattern matching and string search
/hello/i.test("Hello World");
"abc123".match(/\d+/);
(func $__module_init
  global.get $re
  string.const "Hello World"
  call $regexp_test)
Basic patterns work; named groups and lookbehind are the harder cases.
Property accessors (get / set) Getter and setter property definitions
const obj = {
  _v: 0,
  get value() { return this._v; },
  set value(v) { this._v = v; }
};
(func $get_value (param (ref null $obj))
    (result f64)
  local.get 0
  struct.get $obj $_v
)

(func $set_value (param (ref null $obj)) (param f64)
  local.get 0
  local.get 1
  struct.set $obj $_v)
Basic get/set accessors work; Object.defineProperty covers the common descriptors.
Object.defineProperty (full) Full property descriptor configuration
Object.defineProperty(obj, "x", {
  enumerable: false,
  writable: false
});
Property descriptor system not yet fully emitted in Wasm structs.
ES2015
Arrow functions Concise function syntax with lexical this
const add = (a, b) => a + b;
const square = x => x * x;
const greet = () => "hello";
(func $add (param f64 f64)
    (result f64)
  local.get 0
  local.get 1
  f64.add)

(func $square (param f64)
    (result f64)
  local.get 0
  local.get 0
  f64.mul)
Template literals String interpolation with backtick syntax
const msg = `Hello ${name}, you are ${age}!`;
(func $__module_init
  string.const "Hello "
  local.get $name
  string.concat
  string.const "!"
  string.concat)
Destructuring Extract values from arrays and objects
const { name, age } = person;
const [first, ...rest] = items;
const { a: x = 0 } = opts;
;; typed object → direct struct ...
(func $__module_init
  local.get $obj
  struct.get $Obj $a     ;; a
  local.get $obj
  struct.get $Obj $b)    ;; b
Spread / rest operators Expand iterables and collect arguments
const merged = [...a, ...b];
const clone = { ...original };
function sum(...args) { }
(func $__module_init
  ;; [...arr, 4]
  f64.const 1  f64.const 2  f64.const 3
  f64.const 4
  array.new_fixed $f64arr 4)
Default parameters Fallback values for function parameters
function greet(name = "world") { }
(func $greet (param externref)
    (result externref)
  local.get 0
  ref.is_null
  (if (then
    global.get 1
    local.set 0))
  local.get 0
  return)
Computed property names Dynamic property keys in object literals
const key = "id";
const obj = { [key]: 42 };
(func $__module_init
  ;; { [key]: 42 }
  global.get $key          ;; "id"
  f64.const 42
  struct.new $obj)
for-of Iterate over iterable objects
for (const item of iterable) { }
(func $__module_init
  ;; for (const x of arr)
  local.get $arr
  struct.get $Vec $data
  struct.get $Vec $len
  (loop $for_of
    local.get $i
    array.get $data
    ;; ... process x
    br_if $for_of))
Generators (function*, yield) Pausable functions that produce sequences
function* range(n) {
  for (let i = 0; i < n; i++) {
    yield i;
  }
}
(func $range (param f64)
    (result (ref $Gen))
  ;; yield values into WasmGC array
  array.new_default $f64arr 0
  local.set $buf
  (loop $loop
    local.get $i
    local.get 0            ;; n
    i32.lt_s
    (if (then
      ;; yield i → push to buffer
      local.get $i
      ;; i++, br $loop
    )))
  local.get $buf
  struct.new $Generator)
Classes Class declarations with inheritance
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this.name + " speaks";
  }
}

class Dog extends Animal { }
(type $Animal (struct
  (field $__tag i32)
  (field $name externref)))

(func $Animal_new (param externref)
    (result (ref null $Animal))
  struct.new $Animal
  local.get 0
  struct.set $Animal $name)
Constructor, methods, extends, super work. Dynamic prototype lookup is partial.
host Map / Set Key-value and unique-value collections
const m = new Map();
m.set("key", 42);

const s = new Set([1, 2, 3]);
s.has(2); // true
(func $__module_init
  call $Map_new
  global.set $m
  global.get $m
  string.const "key"
  f64.const 42
  call $Map_set)
Core operations work. Some iteration edge cases incomplete.
host Symbol Unique, immutable primitive identifiers
const id = Symbol("id");
obj[id] = 42;
(func $__module_init
  global.get $nextId
  i32.const 1
  i32.add
  global.set $nextId
  global.get $nextId
  global.set $id)
Creation and basic use work. Well-known symbols (Symbol.iterator) partial.
host TypedArray / ArrayBuffer Binary data buffers and typed views
const buf = new ArrayBuffer(16);
const view = new Int32Array(buf);
view[0] = 42;
(func $__module_init
  i32.const 16
  call $ArrayBuffer_new
  call $Int32Array_from
  global.set $view
  global.get $view
  i32.const 0
  f64.const 42
  call $TypedArray_set)
Int8 through Float64 arrays work. BigInt64Array not yet.
Modules (import / export) ES module system for code organization
import { foo } from "./mod";
export function bar() { return 1; }
(func $bar (export "bar")
    (result f64)
  f64.const 1
  return)
Static imports work. Dynamic import() not yet.
Proxy / Reflect Object behavior interception and reflection
Reflect.get(obj, "k");      // works
new Proxy(t, handler);      // trap dispatch not yet
Reflect is largely supported. Proxy trap interception needs runtime dispatch, so handler traps don't compile ahead of time yet.
host Promise .then / .catch / .finally Promise chaining and error handling
Promise.resolve(1).then(v => v + 1); // callbacks need host microtasks
Promise construction and Promise.resolve/all/race work. Chained .then/.catch/.finally callbacks need the host microtask queue to run.
ES2017
host async / await Asynchronous functions with synchronous-style syntax
await fetchValue();          // works on a JS host
await import("./module");    // dynamic import not yet
(func $fetchData (result f64)
  f64.const 42
  call $Promise.resolve
  call $__await
  return)
Object.entries / values Extract entries or values from objects
Object.entries({ a: 1, b: 2 });
// [["a", 1], ["b", 2]]

Object.values({ x: 10 });
// [10]
(func $__module_init
  f64.const 1  f64.const 2
  struct.new $obj
  extern.convert_any
  call $Object.entries)
host SharedArrayBuffer / Atomics Shared memory and atomic operations
new SharedArrayBuffer(1024);
Requires shared Wasm linear memory.
ES2018
Object spread / rest Object spread in literals and rest in destructuring
const clone = { ...original,
  extra: 1 };
const { a, ...rest } = obj;
(func $__module_init
  ;; { ...original, extra: 2 }
  local.get $original
  struct.get $Obj $x
  f64.const 2
  struct.new $Clone)
host Async iteration (for-await-of) Iterate over async data sources
for await (const chunk of stream) { }
(func $process (param externref)
  local.get 0
  call $__iterator
  local.set $iter
  (loop $for_await
    local.get $iter
    call $__iterator_next
    ;; ... process chunk
    br_if $for_await))
Basic patterns work. Some async generator edge cases incomplete.
ES2020
Optional chaining (?.) Safe property access on nullable values
const name = user?.profile?.name;
(func $test (param externref)
    (result externref)
  local.get 0
  ref.is_null
  (if (result externref)
    (then ref.null extern)
    (else local.get 0
          call $__extern_get_name)))
Nullish coalescing (??) Default values for null or undefined
const val = input ?? "default";
(func $test (param f64)
    (result f64)
  local.get 0        ;; x
  call $__is_nullish
  (if (result f64)
    (then f64.const 0)
    (else local.get 0)))
host globalThis Universal global object reference
globalThis.console;
(func $__module_init
  call $__get_globalThis
  global.set $g)
host BigInt Arbitrary precision integer arithmetic
const big = 9007199254740993n;
const sum = big + 1n;
(func $__module_init
  i64.const 9007199254740993
  global.set $big
  global.get $big
  i64.const 1
  i64.add
  global.set $sum)
Basic arithmetic works. BigInt typed arrays not yet.
host Dynamic import() Load modules at runtime on demand
const mod = await import("./module"); // not yet
Requires a runtime module loader.
ES2021
host WeakRef / FinalizationRegistry Weak references and GC callbacks
const r = new WeakRef(obj); // constructs; deref()/cleanup need host GC
Constructors and the basic API compile. GC-observable behavior (deref timing, FinalizationRegistry callbacks) depends on host GC hooks not yet exposed by standard WasmGC.
ES2022
Class fields (public, private, static) Declarative field syntax in classes
class Counter {
  count = 0;
  #secret = 42;
  static instances = 0;
}
(type $Counter (struct
  (field $__tag i32)
  (field $count f64)))

(func $Counter_new
    (result (ref null $Counter))
  i32.const 0     ;; __tag
  f64.const 0     ;; count = 0
  struct.new $Counter)
Error.cause Chain errors with a cause property
throw new Error("fail", { cause: origErr });
(func $fail
  i32.const 1
  string.const "root"
  struct.new $Error        ;; cause
  i32.const 1
  string.const "fail"
  struct.new $ErrorCause
  throw 0)
Array.at / String.at Relative indexing with negative support
[1, 2, 3].at(-1);  // 3
"hello".at(0);      // "h"
(func $__module_init
  ;; [1,2,3].at(-1)
  local.get $arr
  i32.const -1
  local.get $len
  i32.add
  array.get $data)         ;; → 3
Top-level await Await at module scope without async wrapper
const data = await fetch(url);
export { data };
Requires a module-level async execution model.
ES2016
Array.prototype.includes Check if array contains a value
[1, 2, 3].includes(2); // true
;; linear scan over WasmGC array
local.get $arr
struct.get $Vec $data
(loop $scan
  array.get $data
  local.get $target
  f64.eq
  br_if $found
  br $scan)
Exponentiation operator (**) Power operator as infix syntax
const sq = 2 ** 10; // 1024
f64.const 2
f64.const 10
call $Math.pow
ES2019
Optional catch binding Omit the error parameter in catch
try { riskyOp(); }
catch { handleError(); }
(try
  (do call $riskyOp)
  (catch_all
    call $handleError))
host Array.prototype.flat / flatMap Flatten nested arrays
[[1, 2], [3]].flat();
// → [1, 2, 3]
Basic flat() works via host import. Deep flattening and flatMap partial.
host Object.fromEntries Create object from key-value pairs
const obj = Object.fromEntries(
  [["a", 1], ["b", 2]]
);
Uses host import to construct object from entries array.
ES2023
host Array.findLast / findLastIndex Search arrays from the end
[1, 2, 3,
  2].findLast(x => x === 2);
// → 2 (last match)
Iterates WasmGC array from end. Callback via host import.
host Change array by copy (toSorted, toReversed, toSpliced) Immutable array transformations
const sorted = arr.toSorted();
const rev = arr.toReversed();
Immutable array operations require allocating and copying into a new array.
Hashbang (#!) comments Unix shebang line support
#!/usr/bin/env node
console.log("hello");
;; hashbang stripped at parse time
;; no Wasm output for comments
ES2024
host Promise.withResolvers Create Promise with external resolve/reject
const { promise, resolve, reject }
  = Promise.withResolvers();
Static Promise helper; the standalone path needs a host import.
host Resizable ArrayBuffer Growable and shrinkable buffers
const buf = new ArrayBuffer(8,
  { maxByteLength: 16 });
buf.resize(16);
Growable buffers require linear-memory integration.
RegExp v flag Set notation in character classes
/[\p{Letter}--[a-z]]/v.test("A");
Unicode set notation in character classes. Requires updated regex host.
ES2025
host Set methods (union, intersection, difference) Built-in set operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // {1,2,3,4}
ES2025 Set methods. Requires host import for Set operations.
host Iterator helpers (map, filter, take) Lazy iterator transformations
function* nums() { yield 1; yield 2; yield 3; }
nums().filter(x => x > 1)
      .take(1)
      .toArray();
Lazy iterator protocol. Requires iterator helper host imports.
RegExp duplicate named groups Same group name in alternatives
/(?<v>a)|(?<v>b)/.exec("b");
// v === "b"
Same named group in alternatives. Requires regex engine update.
Legacy / Deprecated
var hoisting Function-scoped variable declarations hoisted to top
console.log(x); // undefined
var x = 5;
;; var hoisted to function scope
(local $x f64)
f64.const 0
local.set $x
;; ... later
f64.const 5
local.set $x
arguments.callee Reference to currently executing function
function f() {
  return arguments.callee;
}
Forbidden in strict mode. All modules run strict.
__proto__ accessor Legacy prototype chain manipulation
obj.__proto__ = parent;
For dynamic prototype assignment, use Object.create() or class extends.
String.prototype.substr Deprecated substring extraction
"hello".substr(1, 3); // "ell"
// use .slice() or .substring()
Deprecated. Use .slice() which is fully supported.
Octal literals (0777) Legacy octal number syntax
const n = 0777;
const ok = 0o777;
Legacy octal forbidden in strict mode. ES2015 0o prefix works.
escape() / unescape() Deprecated string encoding globals
escape("hello world");
// use encodeURIComponent()
Deprecated globals. Use encodeURIComponent / decodeURIComponent.
Function.prototype.caller Access calling function reference
function f() {
  return f.caller;
}
Forbidden in strict mode. Not available in Wasm.
HTML string methods (.bold(), .anchor()) Deprecated HTML wrapper string methods
"text".bold();   // deprecated
"text".anchor("name");
Deprecated Annex B legacy HTML-wrapper string methods.
RegExp.$1 static properties Legacy RegExp match result globals
/(\d+)/.exec("abc123");
RegExp.$1; // "123" — deprecated
Legacy static properties. Use match result array instead.
Proposals
Temporal Modern date and time API
const now = Temporal.Now.instant();
const date = Temporal.PlainDate
  .from("2026-04-06");
Stage 3 proposal. Large API surface — not yet in scope.
Decorators Class and method metadata annotations
@logged
class MyClass {
  @bound method() { }
}
Stage 3 proposal. Requires compile-time decorator application.
Pattern matching Structural matching expressions
match (value) {
  when ({ x, y }): return x + y;
  when (String): return value.length;
  default: return 0;
}
Stage 1 proposal. Structural pattern matching not yet in scope.

Tiny modules that load and run fast.

When running outside of a JS engine like found in browsers or Node.js, compatibility today typically means embedding a JS interpreter inside the WebAssembly module. This increases module size and slows execution.

These are microbenchmarks — small single-kernel programs (tight numeric loops, string hashing, array and object operations) that each isolate one class of operation. They indicate where ahead-of-time compilation helps or hurts at the kernel level; they are not full-application workloads, so the ratios shouldn't be read as a general speedup. Each figure is the median of multiple measured rounds taken after warm-up.

JavaScript host

Speedup of JS² compiled WebAssembly relative to the same source code running as JavaScript in V8 (Node.js). Bars extending past the JS baseline mean Wasm is faster; bars falling short mean slower. The left chart pins both runtimes to their baseline tier — the no-optimizing-JIT execution path that matters for cold-start, multi-tenant edge runtimes, and engines without a top tier. The right chart is the production setup — both runtimes use their full optimizing pipeline. Press the button to run the benchmark in your browser including additional DOM benchmarks. These public website benchmarks run in a JS host: the compiled Wasm is instantiated by the js2wasm JavaScript runtime and compared with the same source running in V8. Wasmtime/WASI benchmark figures are shown separately in the WASI host section below.

WASI host

Per-request cost of running the same JavaScript programs (source for each is shown inline below) across four execution models on edge: AOT (Cranelift-compiled .cwasm , no runtime codegen), JS in V8 (Ignition → Liftoff → TurboFan, all at request time, the implicit 1.0× baseline), Interpreter (a per-function tiny module delegating to a preloaded embedded-interpreter plugin), and Engine (a full JS engine bundled inside Wasm with AOT pre-init — multiple megabytes per function). Two scenarios per program: fresh context or instance per request (cold) and reused context or instance (warm). Interpreter and Engine both run a JS interpreter inside Wasm — the size and per-call cost tradeoff isn't free. One chart per benchmark; each chart shows the available execution-model lanes for both scenarios.

Speed (higher is better)

These lanes contrast two production edge-runtime architectures — an AOT-compiled Wasm edge runtime against a V8-isolate edge runtime — rather than any named commercial platform. Cold speed shows warm-engine per-request setup plus the first call: a Rust Wasmtime host compiles each Wasm module or component once, then creates a fresh store and instance per request; JS creates a fresh V8 context, compiles the program into it, and runs once. Warm speed shows steady-state per-call cost on a hot workload after warmup, with bars as speedup over the warm JS-in-V8 baseline.

Fibonacci loop

Tight numeric loop with bit-ops and integer addition. Exercises hot-loop arithmetic throughput — the kind of inner kernel where AOT is typically strongest.

Show source
export const benchmark = {
  id: "fib",
  label: "Fibonacci loop",
  coldArg: 5000,
  runtimeArg: 20000000,
  coldRuns: 7,
  runtimeRuns: 5,
};

/** @param {number} n @returns {number} */
export function run(n) {
  let a = 0;
  let b = 1;
  for (let i = 0; i < n; i++) {
    const next = (a + b) | 0;
    a = b;
    b = next;
  }
  return a | 0;
}

Fibonacci recursion

Exponential recursive calls (no memoization). Exercises function-call overhead, call-stack depth, and small-function dispatch.

Show source
export const benchmark = {
  id: "fib-recursive",
  label: "Fibonacci recursion",
  coldArg: 10,
  runtimeArg: 30,
  coldRuns: 7,
  runtimeRuns: 5,
};

function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

/** @param {number} n @returns {number} */
export function run(n) {
  return fib(n);
}

Array fill + sum

Fill an array with hashed integers then sum it. Exercises array allocation, indexed write+read, and a second tight scalar loop.

Show source
export const benchmark = {
  id: "array-sum",
  label: "Array fill + sum",
  coldArg: 2000,
  runtimeArg: 1000000,
  coldRuns: 7,
  runtimeRuns: 5,
};

/** @param {number} n @returns {number} */
export function run(n) {
  const values = [];
  for (let i = 0; i < n; i++) {
    values[i] = ((i * 17) ^ (i >>> 3)) & 1023;
  }

  let sum = 0;
  for (let i = 0; i < values.length; i++) {
    sum = (sum + values[i]) | 0;
  }
  return sum | 0;
}

String build + hash

Build a string via concatenation then compute a rolling hash. Exercises string allocation, charAt/charCodeAt, and a per-character hot loop — currently the worst case for AOT (see issue #1580).

Show source
export const benchmark = {
  id: "string-hash",
  label: "String build + hash",
  coldArg: 100,
  runtimeArg: 20000,
  coldRuns: 7,
  runtimeRuns: 5,
};

/** @param {number} n @returns {number} */
export function run(n) {
  const alphabet = "abcdefghijklmnopqrstuvwxyz012345";
  let text = "";
  for (let i = 0; i < n; i++) {
    const a = (i * 13) & 31;
    const b = (a + 7) & 31;
    text += alphabet.charAt(a);
    text += alphabet.charAt(b);
    text += ";";
  }

  let hash = 0;
  for (let i = 0; i < text.length; i++) {
    hash = (hash * 31 + text.charCodeAt(i)) | 0;
  }
  return hash | 0;
}

Module size (smaller is better)

Raw .wasm bytes you upload, version-control, or push to a registry per function. AOT is the user's source compiled to Wasm directly; Interpreter is a small per-function bytecode that delegates to a preloaded plugin (shared runtime cost not included in this bar); Engine bundles a full JS engine into the artifact. JS source baseline is the minified .js file.


Compiled ahead of time, no interpreter.

Write regular JavaScript. The compiler produces a .wasm binary that runs in any WebAssembly host with GC support — browser, server, edge. No interpreter embedded; garbage collection is handled by the host via WasmGC. When deployed with explicit imports, the module boundary can reduce ambient access and limit supply-chain attack surface.

The goal is 100% compatibility with the JavaScript and npm ecosystem while adding a module security and module composition layer around compiled units.

JavaScript (0.1 KB gzipped)

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

export function run() {
  return fibonacci(10);
}
app.js
compile

WebAssembly (0.2 KB gzipped)

;; Actual compiler output — number → f64 throughout.
;; No boxing, no anyref, no dispatch overhead.
(func $fibonacci (param f64) (result f64)
  local.get 0
  f64.const 1
  f64.le
  (if
    (then
      local.get 0
      return)
    (else
      local.get 0  f64.const 1  f64.sub
      call $fibonacci
      local.get 0  f64.const 2  f64.sub
      call $fibonacci
      f64.add
      return)))

(func $run (export "run") (result f64)
  f64.const 10
  call $fibonacci)
app.wat

Using DOM and JS APIs

DOM access and browser APIs compile to typed host imports. The compiler generates the bindings — you just write normal JavaScript.

JavaScript (0.1 KB gzipped)

const el = document.createElement("div");
el.textContent = "Hello from Wasm";
document.body.appendChild(el);
dom.js
compile

WebAssembly (0.3 KB gzipped)

(import "env" "global_document"
  (func $doc (result externref)))
(import "env" "Document_createElement"
  (func $createElement
    (param externref externref externref)
    (result externref)))
(import "env" "Element_set_textContent"
  (func $setTC (param externref externref)))
(import "env" "Node_appendChild"
  (func $appendChild
    (param externref externref)
    (result externref)))
(import "env" "Document_get_body"
  (func $getBody (param externref)
    (result externref)))

(func $__module_init
  call $doc
  string.const "div"
  call $createElement
  local.tee $el
  string.const "Hello from Wasm"
  call $setTC
  call $doc
  call $getBody
  local.get $el
  call $appendChild
  drop)
dom.wat

Compile and run

// Compile from JavaScript source
import { compile } from "js2wasm";
import { buildImports } from "js2wasm/runtime";

const result = compile(sourceCode, {
  fileName: "app.js"
});

// Instantiate the compiled module
const imports = buildImports(
  result.imports, undefined, result.stringPool
);
const { instance } = await WebAssembly
  .instantiate(result.binary, imports);
console.log(instance.exports.run()); // → 55
run.ts

compile() produces a .wasm binary and import manifest. Use buildImports() to create the host import object, then instantiate with the standard WebAssembly API.


Frequently asked questions.

How JS 2 treats compatibility, runtime boundaries, architecture decisions, and the difference between compilation and bundling a JavaScript runtime.

Is the compiler production ready?

No. JS 2 is an early-stage research prototype and technical demo, not a production-ready compiler. It is under active development with incomplete language and standard-library coverage, known bugs, and breaking changes between versions. Treat it as something to explore and experiment with — not to deploy in production.

Why not just use a JavaScript engine or interpreter?

Production engines are highly optimized, multi-tier JIT compilers that after warmup approach native performance. JS engines have mature process sandboxes and ship with browsers. JS interpreters are more lightweight but typically 90% slower.

Inside a JS host WebAssembly is no silver bullet and not always faster or even on par with a warmed up JS JIT engine, but it can be in compute heavy scenarios. Also, it adds a module sandbox for untrusted code, isolating it from the host process and other modules to run third party or AI generated code more securely.

Outside of a JS host , embedding a JS engine disables their JIT tier, costing about 90% of performance of a warmed up engine, effectively falling back to their interpreter mode. Shipping an engine alongside typically weights ~14 megabytes. Pure interpreters like QuickJS with ~700kb are more lightweight but 90% slower than a JIT engine. An AOT JS to Wasm approach doesn't have this ceiling and potentially can achieve near-native performance without depending on a JIT or shipping a full engine or interpreter, while start at 100 bytes.

Runtime overhead

In constrained environments — serverless, embedded, or multi-tenant — a JS host can be prohibitively heavy. JIT-disabled engines typically run 10× slower; bundled interpreters add megabytes of dead weight per module. On desktop, shipping a full runtime like Electron adds 100 MB+. WebAssembly modules pay none of these costs.

Dependency management

NPM dependencies are fragile: upgrading one package often cascades across the project, taking weeks, breaking things loudly, or introducing silent bugs — and version conflicts can block upgrades entirely. WebAssembly modules declare explicit imports satisfied per-module, so versions don't need to stay in lockstep across the project.

Supply Chain Attacks

95% of JS code is third-party. A single malicious or buggy dependency can compromise the entire process — exfiltrating data or, in Node.js, accessing the filesystem. Isolation techniques exist but are rarely used due to overhead and complexity. High-profile incidents have hit cornerstone packages like axios and chalk . JS engines also present a large attack surface: despite decades of hardening, zero-days are found continuously, and AI-assisted vulnerability research is accelerating the pace (Anthropic's Glasswing found 271 vulnerabilities in Firefox ). Electron apps must manually patch engine CVEs indefinitely. WebAssembly uses a simpler, deny-by-default model: capabilities are granted explicitly, limiting blast radius significantly.

Why target ECMAScript, not a language subset or superset?
  • Following the ECMAScript standard as the north star maintains full compatibility with the JS and NPM ecosystem. TypeScript annotations are only treated as optimization hints where types can be proven, not as ground truth and do not affect JavaScript runtime behavior.
  • Subsets, supersets, and new languages make compilation easier by subtly breaking the language contract: unsupported dynamic JavaScript is excluded, custom APIs are introduced, or the language is designed around Wasm from the start.

JavaScript is the most widely used programming language. It is easy to use, familiar to millions, and with NPM has the largest ecosystem in the world. It runs everywhere from browsers (e.g. Chrome, Firefox, Safari) to servers (e.g. Node, Deno, Bun) and even on desktop (Electron), mobile, or embedded devices.

While it is not a perfect language due to its legacy and its dynamic nature makes it hard to optimize, it simply is what everyone uses and changing the language - even slightly - would break compatibility with existing codebases and fragment the ecosystem. We think chosing the right tool for the job should not require switching languages or porting code.

Isn't the Test262 conformance still incomplete?

Yes — and the live figures are published openly on the compatibility page rather than rounded up in prose. Two honest caveats matter more than the headline number. First, Test262 measures conformance to the ECMAScript language specification; it does not cover Web APIs, Node.js or other host behavior, or whether an arbitrary real-world npm package runs unchanged. A high pass rate is necessary but not sufficient for “runs real JavaScript.” Second, the standalone (no-JS-host) path is less complete than the JS-host path. Check the per-feature report before relying on anything specific.

Won't you eventually end up re-implementing a JavaScript engine?

That is the real risk, and we treat it as an open empirical question rather than a solved one. The approach is compiled-code-first: resolve as much as possible statically and lower it directly, so the common paths carry no interpreter. The genuinely unstatic corners of the language — eval and dynamic Function construction — would need a small interpreter fallback that runs only on those paths, not a full engine in every module. Whether that fallback can stay small while the rest is compiled, and how much real code can avoid it, is precisely what the prototype is testing. If it turns out the only way to be compatible is to ship an engine, that would be a negative result worth knowing.

Why not just extend an existing typed JavaScript/TypeScript subset language?

Typed JS/TS subset languages compile to small, fast Wasm precisely because they deliberately exclude full ECMAScript: dynamic semantics are dropped by design and the static type system becomes the contract. That is a sound engineering choice, but it is a different goal. Extending one toward full backwards compatibility would mean re-adding the dynamic semantics it was built to avoid — you would be fighting the language's own design. We are targeting existing JavaScript semantics as the north star instead, so that existing code and npm packages are the input, not a rewrite into a new dialect.

What is the realistic timeline to production-ready?

Honestly: unknown. This is research-stage work, and a credible production date would be a guess dressed up as a commitment. The viability of the core bet — full ECMAScript backwards compatibility, AOT-compiled to WasmGC, with no bundled runtime — is still being established. The conformance and benchmark trends are public so progress can be judged from data rather than promises. Until those tell a clearly different story, treat the project as something to evaluate and experiment with, not to deploy.

Do you plan to support all dynamic parts of the language?

We are intending to push the boundaries of what is possible here. The assumption is most code today is actually using very little of the dynamic features, so the goal is to isolate the cost of dynamic language features to the code paths that actually need them. We aim for a seamless experience so the user does not have to think about if code falls within a supported subset. Dynamic features may require guards, boxed representations, host fallbacks, or separately compiled units. You can track our progress in the ECMAScript compatibility reports.

How do you support host APIs like the DOM?

We see these as platform APIs that are imported using safe adapters from the host if in a browser or Node.js environment or polyfilled (e.g. using WASI). In browsers we auto generate adapters for Web APIs and JS builtins from their TypeScript definitions. Providing alternative implementations e.g. in standalone mode is possible by swapping out the import (e.g. Edge.js emulates Node.js APIs WASIX).

Do modules require a JS host to run?

Currently, yes — compiled modules still rely on a JavaScript host and on the host's WebAssembly GC support to run, and standalone (pure-Wasm, no-JS) execution is still incomplete. This is not a fixed design constraint: reducing the dependency on the JS host and on host-provided GC is an explicit goal that is actively being worked on, and the requirement is expected to shrink over time. Standalone mode already runs a growing subset of programs as pure WebAssembly with no JS runtime — you can track progress in the ECMAScript compatibility reports.

Do you depend on Wasm GC?

WasmGC is our initial compilation target. In a supported WebAssembly 3.0 runtime a portable garbage collector (GC) is provided by the host, so runtimes no longer need to implement their own GC in linear memory. They can build on top of a built-in implementation, making modules leaner and interop with the host environment and across components more seamless. WasmGC gives the compiler host-managed structs, arrays, references, and function references that can represent many JavaScript object shapes more directly.

WasmGC is the initial target, but it is not the only possible backend — support for compiling to linear memory may potentially be added in the future for runtimes without GC support.

What are your guiding principles?

Architecture Decision Records document the non-obvious choices behind JS 2 — why WasmGC over linear memory, why ahead-of-time compilation over a JIT, how closures and objects are laid out, and how the compiler keeps JavaScript semantics auditable as it grows.

Architecture Decisions


Project roadmap.

The roadmap is to first establish compatibility with existing code, then expand host support, optimize performance, and strengthen security.