Loading WebAssembly modules

With the ES Module Integration proposal not implemented everywhere yet, here's a comparison of how we can load .wasm modules in different runtimes and compilers. I'm using a wasm file that exports a single function which returns an integer.

(module
  (func $result (result i32) i32.const 42)
  (export "result" (func $result))
)

Browser

In the browser we can fetch (or use XMLHttpRequest) the wasm file contents and instantiate.

fetch('result.wasm')
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes))
  .then(({instance}) => console.log(instance.exports.result()))

Bundlers

Before your code reaches the browser it might go through a bundler, most of which will try to bundle the wasm file next to the output js file.

Webpack

Importing wasm modules is not enabled by default and needs an experimental flag (asyncWebAssembly) to be set. With the flag enabled you can directly and dynamically import wasm modules according to the proposal.

import {result} from './result.wasm' 
console.log(result())
import('./result.wasm')
  .then(({result}) => console.log(result())) 

Vite.js / Rollup

In Vite.js and Rollup, using @rollup/plugin-wasm, wasm modules have a default export which is an initialization function.

import init from './result.wasm'
init()
  .then(({result}) => console.log(result()))

Esbuild

Esbuild does not have a default wasm loader, but contains an example plugin in its documentation. Using the plugin allows you to load wasm the same way as the Rollup example above (using a function as default export).

Node.js

In Node.js, and serverless runtimes based on it (Netlify functions, AWS Lambda, …), modules can be loaded either by reading the file or using the experimental flag --experimental-wasm-modules (v12.3+) when running node.

WebAssembly.compile(require('fs').readFileSync('./result.wasm'))
  .then(WebAssembly.instantiate)
  .then(({exports}) => console.log(exports.result()))

// With --experimental-wasm-modules
import {result} from './result.wasm' 
console.log(result())

Cloudflare workers

As explained in their blog post on modules, with the correct settings (see the wrangler config at the end of the page) the default export of .wasm imports will be a WebAssembly.Module which you can instantiate. For now it seems the .mjs extension is required for your JavaScript modules. Dynamically loading a WebAssembly module results in an error.

import module from './result.wasm' 
const instance = await WebAssembly.instantiate(module)
console.log(instance.exports.result())

// Will error at runtime
import('./result.wasm').catch(console.error) 

Vercel (using Next.js)

Vercel currently has two flavors of serverless JavaScript: serverless functions, which run Node.js via AWS Lambda, and edge functions. In Next.js your api routes are run as serverless functions, so you have access to the full Node.js api, while edge functions are used for middleware and rendering React server components. Setting up Next.js to use WebAssembly requires a change to their Webpack config.

The support for WebAssembly in Next.js 12 is unfortunately pretty bad. I was unable to compile any api route containing WebAssembly imports, the compiler simply hangs. Importing WebAssembly modules in React server components results in cryptic errors:

error - Error: Your page must export a `default` component
error - unhandledRejection: TypeError: Only absolute URLs are supported
error - unhandledRejection: TypeError: Only absolute URLs are supported

With Next.js 11 I'm able to import the WebAssembly exports directly. There's no default export. Dynamically loading WebAssembly modules is supported. I'm not able to test the Edge runtime because the features using it require Next.js 12.

import {result} from './result.wasm' 
console.log(result())
import('./result.wasm')
  .then(({result}) => console.log(result())) 

Conclusion

If, like me, you're trying to ship a Javascript package with some WebAssembly that you'd like to be able to run almost anywhere the safest bet for now is embedding the wasm file in a base64 encoded string. This makes you skip the loading phase completely in exchange for a more bloated filesize.

const wasm =
  'AGFzbQEAAAABBQFgAAF/AwIBAAcKAQZyZXN1bHQAAAoGAQQAQSoLABUEbmFtZQEJAQAGcmVzdWx0AgMBAAA='
// Use a package like base64-arraybuffer here instead of atob to support all runtimes
const bytes = Uint8Array.from(atob(wasm), c => c.charCodeAt(0))
const module = new WebAssembly.Module(bytes)
WebAssembly.instantiate(module)
  .then(({exports}) => console.log(exports.result()))