WebAssembly Was a Compilation Target. These Proposals Want to Make It a Language.
Source: hackernews
WebAssembly shipped in every major browser in 2017. Nine years later, Mozilla is still working on what they call making it a “first-class language on the web.” That gap between “ships in browsers” and “first-class” is worth examining in detail, because it reveals something fundamental about how the web platform works and why adding a new participant to it takes so long.
The core issue is not execution. Wasm executes efficiently and reliably. The issue is integration: how Wasm modules load, how they share types with JavaScript and the browser, and how they access platform APIs like the DOM, fetch, and setTimeout. All of those things currently require JavaScript intermediaries. That is what “first-class” means in this context: being able to do useful web work without relying on JS as a load-bearing layer.
The Instantiation Tax
Loading a Wasm module today requires a specific ceremony:
const response = await fetch('module.wasm');
const { instance } = await WebAssembly.instantiateStreaming(response, {
env: {
log: (ptr, len) => {
const bytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
console.log(new TextDecoder().decode(bytes));
}
}
});
instance.exports.run();
That env object is what wasm-bindgen generates automatically for Rust programs. Emscripten generates a much larger version for C and C++. A non-trivial Rust/Wasm project might have several hundred lines of generated JavaScript glue before any application code runs. The glue handles encoding strings from Wasm linear memory into JavaScript strings, converting JavaScript exceptions into Wasm error codes, and mediating every call that crosses the boundary.
The ES Module Integration proposal replaces this with a single import statement:
import { run } from './module.wasm';
run();
The browser handles compilation, instantiation, and import wiring through the module graph. You declare what the module imports and exports in its type signature, and the module system resolves them the same way it resolves any other module. The implementation complexity is real: ES module evaluation is synchronous, but Wasm compilation is async. The solution threads Wasm into the async module loading phase, the same phase that handles top-level await, so compilation completes before the synchronous evaluation step runs. The proposal has been at Phase 3 for some time and is one of the more visible pieces of the first-class effort.
The Type Boundary Problem
ESM integration solves loading, but not types. Core Wasm has a narrow value type system: integers, floats, and references. When you pass a string from a Wasm module to JavaScript today, the string is encoded as UTF-8 bytes in linear memory with a pointer and a length. The receiving side decodes it. Every string cross-boundary call involves at minimum a copy and a decode.
The JS String Builtins proposal adds a set of importable functions under the wasm:js-string namespace that let Wasm work with JavaScript’s native string type without encoding. In WAT:
(import "wasm:js-string" "concat"
(func $js_concat (param externref externref) (result externref)))
(import "wasm:js-string" "length"
(func $js_length (param externref) (result i32)))
The externref type is an opaque reference to a host (JavaScript) value. The Wasm module does not inspect the string’s bytes; it passes the reference to the host-provided function. Flutter’s Dart-to-Wasm compiler and Kotlin/Wasm have both identified string handling as a significant portion of their JavaScript interop overhead, since idiomatic web code passes strings constantly: DOM queries, attribute names, event types, URLs.
This is also a case where “first-class” means something concrete at the performance level. The encode/decode round trips are not free. For programs that build and manipulate strings frequently, eliminating the UTF-8 copy on every boundary crossing matters.
WasmGC and the Reference Type Foundation
The WasmGC proposal, which shipped in Chrome 119 and Firefox 120 in late 2023, is usually discussed as an enabler for GC’d languages. Before WasmGC, compiling Kotlin or Dart to Wasm meant bundling a custom garbage collector inside the Wasm module, adding hundreds of kilobytes to every binary and duplicating work the browser’s runtime already provides.
But WasmGC also changed the type story for interop. Before it, Wasm had only flat linear memory and numeric types. WasmGC added managed heap types, struct and array with reference semantics that the host garbage collector tracks. A WasmGC struct can hold an externref to a JavaScript object, and JavaScript can hold a reference to a WasmGC struct. Neither side needs to serialize to pass that reference around:
(type $widget
(struct
(field $element externref)
(field $width i32)
(field $height i32)))
(func $create-widget (param $el externref) (result (ref $widget))
(struct.new $widget
(local.get $el)
(i32.const 100)
(i32.const 80)))
The struct lives in the host GC’s heap, holds a live reference to a DOM element, and can be passed back to JavaScript as an opaque handle. This is the foundation the Component Model builds on.
The Component Model and Typed Interfaces
The Component Model is the most ambitious piece of the first-class effort. It defines a module format and an interface definition language (WIT) that describes what a component exports and imports in terms richer than core Wasm’s type system. A WIT interface looks like:
package my:calculator@0.1.0;
interface math {
add: func(a: s32, b: s32) -> s32;
divide: func(a: f64, b: f64) -> result<f64, string>;
format-number: func(n: f64, precision: u8) -> string;
}
The Component Model specifies how these interface types are “lifted” from their low-level Wasm representations when passing into a component, and “lowered” back when passing out. This is the spec-level answer to the wasm-bindgen question: instead of each language’s toolchain generating ad hoc glue, the Component Model defines the canonical transformation for every type. A Rust component and a Kotlin component can interact through a WIT interface without either one knowing what language the other is written in.
Direct Web API access, where a Wasm module imports document.querySelector the same way it imports any other function, requires browser implementations to expose their Web IDL APIs through a WIT-compatible interface. This is the part of the first-class story that is still most incomplete. The intent exists; the browser implementation work is ongoing. It is also the change that would be most visible to developers, since it would mean writing a web application in Rust or Kotlin without any JavaScript in the stack.
Why This Takes So Long
Each proposal requires simultaneous progress across multiple specifications and multiple browser engines. ESM integration touches the HTML spec, the ES module specification, and browser implementations. The Component Model is its own specification, separate from core Wasm, and browser adoption is partial. Web API access through WIT requires changes to how Web IDL is expressed and processed in browsers.
This is why “first-class language” takes years after “ships in browsers.” The core bytecode and execution model landed in 2017, which required a single focused specification effort. The integration surface is a different problem: it requires coordinating with every existing web platform specification simultaneously. The W3C WebAssembly Working Group and the Bytecode Alliance are both involved, each with their own process and stakeholders.
Compare this with how JavaScript was extended. async/await, ES modules, optional chaining, all of these were changes to a single specification (ECMA-262) and touched the platform in contained ways. Adding a new language to the web requires renegotiating the platform’s entire surface.
The WASI Complication
There is a parallel track worth watching: WASI (the WebAssembly System Interface), which is the Component Model applied to server-side and embedded runtimes like Wasmtime and WAMR. WASI has been moving quickly through the Component Model work, defining standard interfaces for filesystems, sockets, and HTTP that any WASI-compatible runtime implements.
The Component Model was designed to be host-agnostic, and in theory a component that imports a WIT http interface works both in a browser and in a WASI runtime. In practice, browser APIs and WASI APIs are different: fetch in the browser carries different semantics than WASI’s wasi:http/outgoing-handler. The convergence path is being worked on but is not complete. For developers who care about writing code that targets both browser and server runtimes, this matters more than the browser-specific integration work.
What Changes When It Lands
When ESM integration, JS String Builtins, and Web API access through the Component Model are all in place, writing a web application in a non-JavaScript language becomes more like writing a web application and less like writing a native application with a JavaScript adapter in front of it. You define interfaces in WIT, compile your language to a component, and the browser wires it to platform APIs through the module system.
The JavaScript layer becomes optional rather than mandatory. Whether that changes what most web developers do is a separate question. JavaScript is not going anywhere, and the Component Model’s composition model may mean Wasm components and JavaScript modules coexist with clean, typed interfaces between them rather than one replacing the other. The outcome where Wasm/JS interop is well-specified and efficient is arguably more useful than the outcome where JS is absent, because most interesting web applications will contain both.
Mozilla’s framing as a multi-year effort is honest. The proposals are at different phases, the implementations are partial, and the coordination surface is large. But the direction is coherent: each piece addresses a specific friction point, and the friction points are well-understood. The nine-year gap between shipping a bytecode format and making it a platform citizen is a useful reminder that the web platform is not one thing, and adding to it requires touching all of its parts.