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.
JS 2 is a research preview of an open source compiler focused on compiling JavaScript to WebAssembly ahead of time, developed at Loopdive.
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:
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.
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.
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.
Dynamic module linking allows to make code more modular so consumers can choose implementations, control upgrades, and swap dependencies for each environment.
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.
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)
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 x === "string"; obj instanceof Array;
;; typeof x === "string" ;; compiles to tag check on struct local.get $x ref.test (ref $String) ;; instanceof → ref.test
const obj = { a: 1, b: 2 };
delete obj.b;
;; delete obj.b local.get $obj ref.null extern struct.set $Obj $b
const x = (1, 2, 3); // x === 3
f64.const 1 drop f64.const 2 drop f64.const 3
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 (const key in obj) {
console.log(key);
}
local.get $obj call $Object.keys local.set $keys ;; iterate keys array
function legacy() {
return arguments.length;
}
// prefer rest params: (...args)
eval("1 + 2"); // delegated to host
with (obj) { x; }
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)
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)
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 {
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 new Error("something went wrong");
(func $__module_init i32.const 1 string.const "something went wrong" struct.new $Error throw 0)
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)
"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)
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)
const obj = JSON.parse('{"a": 1}');
const str = JSON.stringify({ a: 1 });
(func $__module_init global.get 0 call $JSON.parse global.set $obj)
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)
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 => x... f64.const 1 f64.const 2 f64.const 3 array.new_fixed 3 ref.func $callback call $__arr_map
/hello/i.test("Hello World");
"abc123".match(/\d+/);
(func $__module_init global.get $re string.const "Hello World" call $regexp_test)
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)
Object.defineProperty(obj, "x", {
enumerable: false,
writable: false
});
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)
const msg = `Hello ${name}, you are ${age}!`;
(func $__module_init string.const "Hello " local.get $name string.concat string.const "!" string.concat)
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
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)
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)
const key = "id";
const obj = { [key]: 42 };
(func $__module_init
;; { [key]: 42 }
global.get $key ;; "id"
f64.const 42
struct.new $obj)
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))
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)
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)
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)
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)
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)
import { foo } from "./mod";
export function bar() { return 1; }
(func $bar (export "bar")
(result f64)
f64.const 1
return)
Reflect.get(obj, "k"); // works new Proxy(t, handler); // trap dispatch not yet
Promise.resolve(1).then(v => v + 1); // callbacks need host microtasks
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({ 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)
new SharedArrayBuffer(1024);
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)
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))
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)))
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)))
globalThis.console;
(func $__module_init call $__get_globalThis global.set $g)
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)
const mod = await import("./module"); // not yet
const r = new WeakRef(obj); // constructs; deref()/cleanup need host GC
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)
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)
[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
const data = await fetch(url);
export { data };
[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)
const sq = 2 ** 10; // 1024
f64.const 2 f64.const 10 call $Math.pow
try { riskyOp(); }
catch { handleError(); }
(try
(do call $riskyOp)
(catch_all
call $handleError))
[[1, 2], [3]].flat(); // → [1, 2, 3]
const obj = Object.fromEntries( [["a", 1], ["b", 2]] );
[1, 2, 3, 2].findLast(x => x === 2); // → 2 (last match)
const sorted = arr.toSorted(); const rev = arr.toReversed();
#!/usr/bin/env node
console.log("hello");
;; hashbang stripped at parse time ;; no Wasm output for comments
const { promise, resolve, reject }
= Promise.withResolvers();
const buf = new ArrayBuffer(8,
{ maxByteLength: 16 });
buf.resize(16);
/[\p{Letter}--[a-z]]/v.test("A");
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // {1,2,3,4}
function* nums() { yield 1; yield 2; yield 3; }
nums().filter(x => x > 1)
.take(1)
.toArray();
/(?<v>a)|(?<v>b)/.exec("b");
// v === "b"
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
function f() {
return arguments.callee;
}
obj.__proto__ = parent;
"hello".substr(1, 3); // "ell" // use .slice() or .substring()
const n = 0777; const ok = 0o777;
escape("hello world");
// use encodeURIComponent()
function f() {
return f.caller;
}
"text".bold(); // deprecated
"text".anchor("name");
/(\d+)/.exec("abc123");
RegExp.$1; // "123" — deprecated
const now = Temporal.Now.instant();
const date = Temporal.PlainDate
.from("2026-04-06");
@logged
class MyClass {
@bound method() { }
}
match (value) {
when ({ x, y }): return x + y;
when (String): return value.length;
default: return 0;
}
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.
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.
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.
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.
Tight numeric loop with bit-ops and integer addition. Exercises hot-loop arithmetic throughput — the kind of inner kernel where AOT is typically strongest.
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;
}
Exponential recursive calls (no memoization). Exercises function-call overhead, call-stack depth, and small-function dispatch.
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);
}
Fill an array with hashed integers then sum it. Exercises array allocation, indexed write+read, and a second tight scalar loop.
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;
}
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).
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;
}
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.
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.
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1)
+ fibonacci(n - 2);
}
export function run() {
return fibonacci(10);
}
app.js
;; 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
DOM access and browser APIs compile to typed host imports. The compiler generates the bindings — you just write normal JavaScript.
const el = document.createElement("div");
el.textContent = "Hello from Wasm";
document.body.appendChild(el);
dom.js
(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 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.
How JS 2 treats compatibility, runtime boundaries, architecture decisions, and the difference between compilation and bundling a JavaScript runtime.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
The roadmap is to first establish compatibility with existing code, then expand host support, optimize performance, and strengthen security.