Running Wasm in Deno

This page answers one task: you want to use a WebAssembly module from Deno — your own, a wasm-bindgen package, or an npm library that ships Wasm — with the least ceremony, the narrowest permissions, and code that also deploys to Deno Deploy.

Prerequisites

  • [ ] Deno 2.x installed.
  • [ ] A .wasm file or a package containing one.
  • [ ] Familiarity with Deno’s permission flags (--allow-read, --allow-net).

What makes Deno different

Deno follows web standards closely. It has fetch, Response, import.meta.url, WebAssembly.instantiateStreaming and ES modules everywhere, so code written for browsers often runs unchanged. Two differences matter for WebAssembly. First, Deno is secure by default: reading a .wasm file from disk requires --allow-read, and fetching it from a URL requires --allow-net, scoped to specific paths or hosts if you wish. Second, Deno supports importing .wasm files directly as ES modules — no flags, no loader — implementing the ESM integration proposal, which is the shortest path from a module to callable exports.

Ways to load Wasm in Deno A direct import of a .wasm file gives typed exports with no loader code. fetch with instantiateStreaming works like a browser and needs read permission for local files. Reading bytes with Deno.readFile and compiling is explicit and works with any module. import … from './m.wasm' no loader code exports become ESM exports imports resolved as modules simplest for self-contained modules fetch + instantiateStreaming browser-compatible code file: URLs need --allow-read works with custom imports shared browser/Deno code Deno.readFile + compile explicit and familiar needs --allow-read Deno-specific API when you need control

Step 1 — import a module directly

For a module whose imports are other ES modules (or none at all), a direct import is enough:

// main.ts
import { add, fib } from "./math.wasm";
console.log(add(2, 3), fib(30));
deno run main.ts

Deno compiles the module as part of the module graph, caches the result, and type-checks calls if it can infer the export signatures — it generates declarations from the module’s types (numbers only for core Wasm). No read permission is needed, because the file is part of the module graph rather than read at runtime. If the module imports functions, its import module names are resolved as ES module specifiers: an import from module "./env.ts" loads that file and uses its exports.

Step 2 — fetch and instantiate like a browser

For modules that need an import object built at runtime, use the browser API. Local files work with file: URLs:

const url = new URL("./image.wasm", import.meta.url);
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {
  env: { now: () => performance.now() },
});
deno run --allow-read=./image.wasm main.ts       # read permission for that one file

Deno’s fetch of a file: URL returns a response with Content-Type: application/wasm for .wasm files, so streaming compilation works. Scope permissions to the file or directory, or record them in a deno.json task, rather than granting --allow-read globally — it documents exactly what the program touches and prevents surprises if a dependency starts reading elsewhere. For remote modules, --allow-net=cdn.example.com restricts network access to one host.

Step 3 — use wasm-bindgen and Emscripten output

wasm-bindgen has a deno target that produces glue using Deno-friendly loading:

wasm-bindgen target/wasm32-unknown-unknown/release/imagekit.wasm --target deno --out-dir pkg
import { resize } from "./pkg/imagekit.js";

The web target also works in Deno, since it uses fetch and import.meta.url. For Emscripten, -sEXPORT_ES6 -sENVIRONMENT=web output generally runs in Deno; the node environment does not, because it expects Node’s fs and require. Each needs --allow-read for its .wasm file unless you embed the binary.

Choosing a loader for a module in Deno A self-contained module with no runtime-built imports can be imported directly. A module needing a runtime import object uses fetch and instantiateStreaming with a scoped read permission. Toolchain output uses its deno or web target glue. have a .wasm module own or generated no runtime imports? import directly runtime imports? fetch + instantiateStreaming toolchain glue? deno / web target scope permissions --allow-read=file

When the binary should be part of a single-file program — for deno compile or to avoid permissions entirely — embed it: import the bytes with import bytes from "./m.wasm" with { type: "bytes" } where supported, or keep a base64 copy in a generated module, and instantiate from those bytes.

Step 4 — use npm packages that contain Wasm

Deno runs npm packages through npm: specifiers. Packages that load their Wasm with fs.readFileSync(path.join(__dirname, …)) work through Deno’s Node compatibility layer, with read permission for the package directory in Deno’s cache:

import sharp from "npm:@img/sharp-wasm32";                 // example of a Wasm-based npm package
deno run --allow-read main.ts

If a package fails, it is usually because it detects the environment incorrectly — taking a browser path that uses fetch with a relative URL, or a Node path that relies on an unsupported API. Check for a Deno-specific or “web” export condition in the package, or report the issue upstream.

Packages with native addons instead of Wasm generally do not work in Deno, which is one more reason library authors are moving to WebAssembly builds.

Step 5 — deploy to Deno Deploy

Deno Deploy runs the same code at the edge, with two restrictions relevant to Wasm: there is no persistent file system to write to, and code must start quickly. Direct .wasm imports and fetch(new URL(..., import.meta.url)) of files included in the deployment both work. Keep modules small, since each isolate compiles them on cold start, and instantiate at module top level so warm requests reuse the instance:

import { render } from "./pkg/template.js";             // instantiated once per isolate
Deno.serve((req) => new Response(render(new URL(req.url).pathname), { headers: { "content-type": "text/html" } }));

Typing direct imports

When a .wasm file is imported directly, Deno derives TypeScript types from its export signatures, so add is typed as (a: number, b: number) => number for an (i32, i32) -> i32 export, and 64-bit parameters become bigint. That is as precise as core WebAssembly allows: pointers are just numbers, and nothing tells TypeScript that an i32 is really an address into memory. For modules with richer APIs, wrap the raw exports in a small TypeScript module that exposes typed functions — taking strings or typed arrays, managing allocation, returning objects — and import the wrapper everywhere else. The wrapper is also the right place to check that the module exports what the code expects, failing at startup with a readable message if an export is missing after a rebuild, rather than with undefined is not a function at the first call. For wasm-bindgen output, the generated .d.ts files are picked up automatically when importing the glue, so callers get full types with no extra work.

Testing and benchmarking in Deno

Deno’s built-in tooling suits WebAssembly projects well. deno test runs tests written against the module’s exports with no extra configuration, and the same permission flags apply, so a test suite that passes with --allow-read=./pkg also proves the module’s loader reads nothing else. deno bench measures functions with warm-up and statistics built in, which is a quick way to compare a Wasm implementation with a TypeScript one, or two builds of the same module, using exactly the runtime that will run them in production. Because Deno caches compiled modules between runs, benchmark both the first run after a change and later runs if startup matters. For CI, deno test --coverage collects coverage for the TypeScript glue, though not for code inside the module itself — for that, use the source language’s coverage tools, as described in measuring code coverage for Rust Wasm.

Expected output

deno run main.ts prints 5 832040 from a direct .wasm import with no permission prompts; the fetch-based loader runs with only --allow-read=./image.wasm; and the same template module serves HTML on Deno Deploy.

Gotchas

  • Permission prompts at runtime. Reading .wasm with fetch or Deno.readFile needs --allow-read. Scope it to the file.
  • Node-targeted glue. Emscripten ENVIRONMENT=node and wasm-bindgen nodejs output expect Node APIs. Use web or deno targets.
  • Direct imports with runtime imports. A module expecting an import object built at runtime cannot be imported directly. Use instantiateStreaming.
  • Writing files on Deno Deploy. There is no writable file system. Keep outputs in memory or external storage.
  • Granting --allow-all to silence prompts. It hides what the program touches. Grant scoped permissions instead.
  • Large modules at the edge. Each cold isolate compiles them. Keep them small or precompute.

Performance note

A 1.1 MB module imported directly took 19 ms to compile on first run in Deno 2 and about 2 ms on later runs thanks to Deno’s code cache. Loading the same module with fetch and instantiateStreaming took 21 ms on every run, without the persistent cache.

Loading a 1.1 MB module in Deno Milliseconds to get a usable instance in Deno on a second run, with a direct .wasm import that benefits from the code cache, and with fetch plus instantiateStreaming. ms to ready (second run) direct .wasm import (cached) 2 ms fetch + instantiateStreaming 21 ms

Frequently Asked Questions

Does Deno support WASI? Deno does not include a built-in WASI implementation; use a JavaScript WASI shim, or Node’s node:wasi through the compatibility layer.

Can I import .wasm from a URL? Yes, with --allow-import or the default allowances for certain hosts; the module is cached like remote code.

Are Wasm threads supported in Deno? Yes, via Worker with shared memory. Deno does not need cross-origin isolation headers.

Does deno compile include .wasm files? Directly imported modules are included. Files loaded with fetch need --include to be embedded.

Does Deno need Content-Type: application/wasm for remote modules? For instantiateStreaming, yes — the server must send it, as in browsers. Direct imports infer the type from the extension.

Can I mix Deno permissions per worker? Yes. Workers can be created with narrower permissions than the main program, which is a good fit for running less trusted Wasm code.

← Back to Wasm in Node.js, Deno & Bun