Download PDF version of this article PDF

When Is WebAssembly Going to Get DOM Support?

Or, how I learned to stop worrying and love glue code

Daniel Ehrenberg

Is WebAssembly (Wasm) really ready for production usage in web applications, even though that usage requires integration with a web page and the APIs used to manipulate it, such as the DOM?

Simultaneously, the answer to this question is that "Wasm might never get direct DOM access," and "Yes, Wasm is ready for all kinds of web-integrated uses, having supported calling out to the DOM (with a little indirection) since day zero." Let me explain.

WebAssembly was designed from its outset to have a strict separation from JavaScript. Unlike asm.js (developer.mozilla.org), which is unavoidably embedded in JavaScript, the core Wasm bytecode format is completely clean of all the "legacy" of JavaScript. At the same time, it says it right in the name—Wasm is part of the web platform. So, why are web APIs like the DOM still tied to JavaScript, and why are there no new Wasm versions?

The answer is that new versions of web APIs, such as the DOM, are not needed to make them usable from Wasm; the existing JavaScript APIs work just fine. When used in the browser or other JavaScript environments, Wasm includes various JavaScript APIs that allow compiler-generated glue code to provide seamless access to any JavaScript code.

WebAssembly has been progressively evolving toward allowing build toolchains to emit less code, specifically less JavaScript. These improvements target the code that makes real web applications slow and bulky in Wasm. Wasm's integration with JavaScript will continue to evolve and deepen through the work of the W3C Wasm CG (Community Group). 

Maybe, if it turns out to cause performance and code-size improvements to justify its complexity, a mechanism could be added in the future to call web APIs straight from Wasm, but this mechanism would require tremendous effort to design and implement. On the other hand, a bit of JavaScript glue to connect web APIs might not be the bottleneck to worry about, especially when there are other sources of overhead to address.

 

Gluing Wasm to JavaScript

To understand how Wasm and JavaScript fit together, let's work through an example adapted from Wasm's excellent documentation on MDN (developer.mozilla.org) showing how Wasm can call console.log. Though Wasm's main format is a binary bytecode, the specification also defines a text format called WAT (Wasm text) that makes it easy to see what's going on:

(module
  (func $log (import "myConsole" "myLog") (param i32))
  (func (export "log42")
    i32.const 42
    call $log))

Wasm is a module language: Each Wasm module declares imports and exports. To run a module, youinstantiate it with a set of imports, and this operation provides the set of exports. The Wasm JavaScript API makes this module instantiation functionality accessible from JavaScript.

This example is a simple module that imports a function called myLog from the module myConsole. It exports a function log42 that calls that function with the argument 42. JavaScript code can call any web API by referencing the global object, but Wasm has access only to what you give it, so console.log is threaded through to Wasm through a module import:

const importObject = {
  myConsole: {
    myLog(arg) {
      console.log(arg);
    }
  }
};
const { instance } = await WebAssembly.instantiateStreaming(
    fetch("simple.wasm"), importObject);
instance.exports.log42();

The idea is that build toolchains that output Wasm for websites will actually output a combination of Wasm and JavaScript, with the latter responsible for loading the module and linking it with the appropriate JavaScript capabilities, such as this myConsole module.

 

Modeling Everything with Flat Memory

To paraphrase German mathematician Leopold Kronecker, God created i32, i64, f32, and f64; all the rest is the work of man.

WebAssembly started with a simple built-in type system. As far as values that can live on the stack and be passed as arguments to functions, it initially supported just what would fit in its flat, bitmapped memory: the four byte-addressable numeric types just mentioned. It turns out, however, that this gives Wasm bytecode full flexibility to call JavaScript APIs, even those that take objects as arguments, rather than just numbers as in the console.log example.

Here's how you do it:

  1. Maintain an array in JavaScript that contains all of the references to the JavaScript objects the Wasm program needs to concern itself with.
  2. The modules that JavaScript exports to Wasm are wrapped. When a function is expected to take an object as an argument and return a different object, the wrapper will accept and return integer indices into that array, allocating a new entry for the return value.
  3. Wasm code will explicitly free these references when it no longer needs them by calling another function imported from JavaScript that manages a free list.
  4. The final artifact is a bundle of JavaScript and Wasm, not just Wasm.

"That's cheating," I hear you protest. "That's not Wasm, it's just a Rube Goldberg machine of JavaScript with a little bit of Wasm in the middle!" Maybe so, but it has always been the plan to get the Wasm project bootstrapped, and it still lets you write your core logic in Wasm.

This minimal design comes copy-pasted from the successful experience of asm.js. It is informed by the fate of Chrome's NaCl (Native Client) effort (web.archive.org), which introduced the new Pepper system for doing I/O, and which did not get cross-browser support. (NaCl has now been deprecated, and its documentation page is a 404, so you need to go to the Internet Archive Wayback Machine to even learn about it).

This all comes back to Wasm's original goal: to enable more programs to run with good performance on the web, rather than remove every last bit of JavaScript. The core of a program, compiled to Wasm, might be the most important part to optimize, based on how slow or bulky it would have been when written in JavaScript. Shipping that core in Wasm rather than JavaScript might be the key to making the application work in practice. That benefit isn't negated by a little JavaScript around the edges.

Core to this design is the idea that Wasm is built with a compiler toolchain that can establish conventions and generate such matching code across the JavaScript/Wasm boundary. Many of these compiler toolchains have been created and are in wide use, including ones for C/C++ (emscripten.org), Rust (rustwasm.github.io), Kotlin (kotlinlang.org), Java (developer.fermion.com), and more. Each one manages to thread web APIs through to Wasm in this sort of manner, so the application developer doesn't need to worry about it.

 

It Gets Worse before It Gets Better

If you think about it, you can basically model everything in this mixed JavaScript/Wasm design. Here are a few examples that have been implemented in actual Wasm build toolchains.

 

For exceptions

Call an imported JavaScript function to throw an exception (not that bad) and to model a try/catch block (this is pretty bad):

 

For blocking I/O

Many programs you might want to compile to Wasm are written with synchronous blocking behavior, but JavaScript (and the web platform, more broadly) is nonblocking and is instead based on JavaScript running to completion without waiting on I/O, following up later via Promises and callbacks. Two options can bridge these models:

 

For garbage-collected values

In the language being compiled to Wasm, there are two options:

Although you can express everything with the minimal beginnings of Wasm, the whole point of Wasm is to make applications run faster by reducing indirection. Now we're just adding more! For example, the exception-handling scheme described earlier was found to cause a big slowdown in C++ programs ported to Wasm.

 

It Gets Better

The key is to prioritize not "what will let me do this whole thing in Wasm and not JavaScript," but "what will make my programs actually work better," focusing instead on which parts are especially painful in practice in terms of performance or code size. Plus, the strategy has been to work incrementally, adding capabilities little by little, keeping in mind the idea of a larger coherent whole.

Sometimes, implementations can transparently improve performance. For example:

Other times, however, Wasm has needed to add entirely new capabilities to eliminate certain kinds of overhead. These include the following capabilities for exceptions, blocking operations, and garbage-collected values.

 

For exceptions

Capabilities include native exception throwing and try/catch in Wasm (github.com).

 

For blocking operations

WebAssembly has the native ability to pause execution (v8.dev) to wait for a JavaScript Promise.

 

For garbage-collected values

A stack of additions for different parts of support are all based on the idea of sharing the JavaScript heap, rather than encouraging other GCs to be compiled to Wasm:

 

Some good news

Wasm has already added these features (web.dev) at least in the most up-to-date implementations. While none of these features lets you access the DOM without any glue code, they do significantly improve the performance of the kind of code that is likely to want to interact with the DOM: Modern, feature-rich, garbage-collected languages are where most UIs, and new code in general, are built.

 

But What about Direct DOM Access?

The Wasm Component Model has long been discussed as a possible path to more direct access to web APIs. In fact, earlier drafts of the Component Model were called "Web IDL bindings" (github.com).

Web IDL (Interface Description Language) (webidl.spec.whatwg.org) is based on the OMG (Object Management Group) IDL (omg.org), part of CORBA (Common Object Request Broker Architecture). Web IDL was forked to ensure that web APIs could be specified precisely with respect to how they worked in JavaScript, whereas CORBA didn't define JavaScript mappings. Like OMG IDL, Web IDL was initially a cross-language mechanism, supporting not just JavaScript, but also Java—remember, in its early days, Mozilla was working on a Java-based web browser, and Java applets were initially expected to be an important part of the web. Implementations of Web IDL-exposed APIs tend to be written in C++ or Rust. Given this cross-language nature, wouldn't Web IDL be a good basis for Wasm bindings?

 

Web APIs Are JavaScript APIs

It's not so simple to separate Web IDL from JavaScript. Web IDL shed its Java support years ago in order to evolve in a more agile way toward enabling more "JavaScript-y" APIs, with support for iterators, Promises, namespaces, etc., which would be more difficult to explain on the Java side in a matching way. It would take some significant thought to truly extract the JavaScript parts of Web IDL and design a more direct Wasm mapping.

All modern web browsers implement most Web IDL-exposed APIs with code generation based on the API's Web IDL definition, ensuring uniformly correct behavior. So, it's just a matter of tweaking this script to generate some extra Wasm bindings, right? Not so fast—there are a couple issues:

Not all web APIs even use Web IDL. For example, the JavaScript specification itself does not define its standard library in Web IDL. Ecma International's TC39, the JavaScript standards committee, has considered adopting an IDL, but it would be nontrivial to do so given various mismatches in conventions applied, not to mention the compatibility risk related to making any observable change to existing JavaScript functionality.

That being said, some JavaScript capabilities would certainly be useful from within Wasm programs. Moreover, some Web IDL?defined APIs are not implemented with a standard Web IDL generator. In most implementations, the Wasm/JavaScript API itself does not use this generator, as it is implemented in JavaScript engines rather than the browser layer.

All considered, it's more accurate to think of web APIs today, including the DOM, as native to JavaScript rather than cross-language.

 

How about the Wasm Component Model?

If we can work through all of that, web APIs could be exposed as Wasm modules, perhaps in a manner similar to the built-in support for JavaScript strings described earlier. But rather than modules that work just one way, it might be more efficient to use the Wasm Component Model.

Different programming languages use different sorts of memory layouts (e.g., strings in linear vs. GC memory). To get the best speedups, you would want to target the most appropriate one directly, rather than doing an extra copy in some cases—this is part of what the Component Model provides. Web API imports via Web IDL and the Component Model are prototyped as part of the Jco project, which brings the Wasm Component Model to JavaScript (bytecodealliance.github.io).

Would this feature actually improve runtime performance or size of distributed applications? While there would probably be some size reduction in terms of less JavaScript glue code generated, it's unclear if it's enough to matter. Additional prototyping would be needed to find out what sorts of benefits would actually be gained in practice.

For now, web folks don't seem to be sold on the urgency of this very large project. There is no active work by browser vendors in this direction. The plan is to give the Component Model effort, led more by server folks, some time to develop, and maybe later see if something useful emerges from that.

One possible future, under development as part of Jco, is for the Component Model and any components representing web APIs to be more of a toolchain convention rather than something built into browsers. In this form, toolchains would have less work to do, relying on shared components to access web APIs/DOM instead of needing to roll their own bindings. When the rubber hits the road, this strategy would mean that you're still going through JavaScript.

Evolving the Wasm Bytecode Format

Like many programming languages, Wasm is developed in a standards committee, a group of experts from different backgrounds who come together to design, debate, prototype, and document new additions to the bytecode.

This standards committee is the W3C Wasm Community Group. As the name suggests, it's part of the World Wide Web Consortium (W3C). "Community Group" indicates that this group is open to the whole developer community. Anyone can sign onto the intellectual property policy and start contributing to this group without paying a W3C membership fee.

Still, that doesn't mean that you can just join and push something through single-handedly. Standards work is about consensus building, which means you need to repeatedly justify the feature's utility and design to various audiences in various ways until (almost) everyone is on board with the idea. It's especially important either to prototype the feature yourself—or to persuade others to do so—early in the process. To make decisions, the Wasm CG uses a lightweight voting process (github.com) that amounts to rough consensus (wikipedia.org).

To facilitate incremental development and prototyping in alignment with the CG, there is a six-phase process (github.com), inspired in part by TC39's stages (tc39.es). The idea is to interleave design, prototype implementations, and consensus building. This way, the group can learn from experience before declaring a standard "final," while enabling draft implementations to be increasingly aligned with the direction of the group as they become increasingly advanced.

Input from different parties is important for developing a good Wasm feature. These different parties include:

If you're in one of these groups, please contribute to the Wasm CG by giving feedback in GitHub's issues for proposal repositories (github.com) and by joining the Wasm CG's regular meetings (github.com). You can join the Wasm CG via its W3C website (w3.org).

 

Is Wasm Complete?

There are still ways to further reduce the overhead of programs that span the JavaScript/Wasm boundary, including:

There's more than that going on in the Wasm CG; for example, this author is particularly excited about the memory control proposal (github.com), which could significantly improve security by making dereferencing a null pointer throw, which has been found to be a security issue (usenix.org). See also, "On Binary Security of Wasm" (wingolog.org).

For each of these features, as well as direct DOM/web API access, browser vendors will decide whether to implement the feature based on whether it improves actual performance or code size. Otherwise, the tools to integrate Wasm into web applications are already there and are being successfully applied.

 

Where Wasm Fits In

What should be relevant for working software developers is not, "Can I write pure Wasm and have direct access to the DOM while avoiding touching any JavaScript ever?" Instead, the question should be, "Can I build my C#/Go/Python library/app into my website so it runs with good performance?" Nobody is going to want to write that bytecode directly, even if some utilities are added to make it easier to access the DOM.

WebAssembly should ideally be an implementation detail that developers don't have to think about. While this isn't quite the case today—JavaScript stubbornly maintains a leg up as something that "Just Works," with web APIs and developer tools designed primarily against it, and JavaScript build toolchains acting as merely an optimization—the thesis of Wasm is, and must be, that it's okay to have a build step.

For the foreseeable future, when this build step is used to deploy Wasm on the web, the build output will have both JavaScript and Wasm.

 

Daniel Ehrenberg is a software engineer on Bloomberg's JavaScript Infrastructure and Tooling team. He serves as the president of Ecma International and contributes to Ecma TC39, the JavaScript standards committee. He has dabbled in Wasm and web standards as well, both while at Bloomberg and in his previous positions at Igalia, a free software cooperative, and at Google on the V8 team, the JavaScript engine in Chrome.

 

Copyright © 2025 held by owner/author. Publication rights licensed to ACM.

acmqueue

Originally published in Queue vol. 23, no. 3
Comment on this article in the ACM Digital Library





More related articles:

Conrad Watt - Concurrency in WebAssembly
Mismatches between the interfaces promised to programmers by source languages and the capabilities of the underlying web platform are a constant trap in compiling to Wasm. Even simple examples such as a C program using the language's native file-system API present difficulties. Often such gaps can be papered over by the compilation toolchain somewhat automatically, without the developer needing to know all of the details so long as their code runs correctly end to end. This state of affairs is strained to its limits when compiling programs for the web that use multicore concurrency features.


Ben Titzer - WebAssembly: How Low Can a Bytecode Go?
Wasm is still growing with new features to address performance gaps as well as recurring pain points for both languages and embedders. Wasm has a wide set of use cases outside of the web, with applications from cloud/edge computing to embedded and cyber-physical systems, databases, application plug-in systems, and more. With a completely open and rigorous specification, it has unlocked a plethora of exciting new systems that use Wasm to bring programmability large and small. With many languages and many targets, Wasm could one day become the universal execution format for compiled applications.


Andy Wingo - WebAssembly: Yes, but for What?
WebAssembly (Wasm) has found a niche but not yet filled its habitable space. What is it that makes for a successful deployment? WebAssembly turns 10 this year, but in the words of William Gibson, we are now as ever in the unevenly distributed future. Here, we look at early Wasm wins and losses, identify winning patterns, and extract commonalities between these patterns. From those, we predict the future, suggesting new areas where Wasm will find purchase in the next two to three years.


Shylaja Nukala, Vivek Rau - Why SRE Documents Matter
SRE (site reliability engineering) is a job function, a mindset, and a set of engineering approaches for making web products and services run reliably. SREs operate at the intersection of software development and systems engineering to solve operational problems and engineer solutions to design, build, and run large-scale distributed systems scalably, reliably, and efficiently. A mature SRE team likely has well-defined bodies of documentation associated with many SRE functions.





© ACM, Inc. All Rights Reserved.