Running Wasm in Bun
This page answers one task: a project uses Bun as its runtime, package manager or bundler, and needs WebAssembly modules — its own, generated glue, or npm packages — to load and run correctly, with an eye on where Bun behaves differently from Node.
Prerequisites
- [ ] Bun 1.1 or newer.
- [ ] A
.wasmmodule, a toolchain’s output, or an npm package that uses Wasm.
Bun’s approach to WebAssembly
Bun runs on JavaScriptCore, Safari’s engine, rather than V8. Its WebAssembly implementation is therefore JavaScriptCore’s — BBQ and OMG tiers instead of
Liftoff and TurboFan — with the standard WebAssembly JavaScript API on top. Bun aims for Node compatibility, so most Node-style loaders work as is, and
adds conveniences of its own: .wasm files can be imported directly, Bun.file() reads files lazily and fast, and the bundler understands .wasm
imports. It also includes a WASI implementation, so a WASI module can run with a single command.
Because the engine differs, performance and edge-case behaviour can differ from Node. A module that relies on a feature JavaScriptCore has not shipped yet, or that is tuned for V8’s tiering, may behave differently. Testing in Bun is therefore not optional if you support it.
Step 1 — load a module the standard way
The portable loader from Node works in Bun unchanged:
import { readFile } from "node:fs/promises";
const bytes = await readFile(new URL("./math.wasm", import.meta.url));
const { instance } = await WebAssembly.instantiate(bytes, { env: { log: console.log } });
console.log(instance.exports.add(2, 3));
Bun’s own API reads files faster and returns an ArrayBuffer directly:
const bytes = await Bun.file(new URL("./math.wasm", import.meta.url)).arrayBuffer();
fetch with a file: URL also works, so the browser-style instantiateStreaming(fetch(url)) pattern runs in Bun as well — useful when the same loader
must work in browsers.
Whichever reader you use, compile once per process and keep the WebAssembly.Module if you need more than one instance — for example one per worker —
exactly as you would in Node.
Step 2 — import .wasm directly
import { add } from "./math.wasm";
console.log(add(2, 3));
Bun compiles and instantiates the module when it is imported. Like Deno’s implementation, this suits modules whose imports are satisfied by other modules
or that have none. For modules that need a runtime-built import object, use the explicit loader from step 1. Direct imports also make the module part of
the dependency graph, so bun build can see and bundle it without extra configuration.
Step 3 — run WASI modules
Bun runs WASI preview 1 command modules directly:
bun run ./wc.wasm input.txt
Programmatically, Bun supports Node’s node:wasi API, so code written for Node, as in
running WASI modules in Node.js,
generally runs as is. Coverage of WASI functions may differ from Node’s; test file-system operations your module relies on, especially directory
listings and renames, which are the least commonly used parts of WASI preview 1.
Step 4 — use toolchain output and npm packages
wasm-bindgen’s web and bundler targets work in Bun; so does nodejs through Bun’s CommonJS support. Emscripten output built with
-sENVIRONMENT=node or web generally runs. npm packages that ship WebAssembly — image codecs, parsers, compression libraries — install with
bun install and load through their normal Node code paths.
When something fails, the cause is usually one of three things: a package that checks process.versions.node or typeof Bun and takes an unexpected
branch; a native addon fallback that Bun cannot load (prefer the package’s Wasm build, often exposed through a wasm32 or -wasm variant); or a Wasm
feature JavaScriptCore does not support yet. Running the package’s own test suite under bun test is a fast way to find out.
Step 5 — bundle for production
bun build resolves .wasm imports and copies the files into the output directory, rewriting references so the bundle can find them:
bun build ./src/index.ts --outdir dist --target bun
For --target browser, the .wasm file is emitted as an asset and loaded at runtime. bun build --compile produces a single executable; modules imported
directly are embedded, while files read at runtime with Bun.file must be included explicitly or shipped alongside. Check the output directory to confirm
the .wasm file is there and that the loader references it by a path that will exist after deployment.
Serving Wasm with Bun.serve
Bun is often used as an HTTP server, and two Wasm-related uses come up. The first is serving .wasm files to browsers: Bun.file() infers
application/wasm from the extension, so new Response(Bun.file("./public/app.wasm")) sends the right Content-Type for streaming compilation, and
Bun handles range requests and conditional requests for static files. Add long-lived Cache-Control headers for content-hashed file names, as for any
static asset. The second is running Wasm inside request handlers — templating, validation, image processing. Instantiate the module once at startup, at
module top level, and reuse the instance across requests; per-request instantiation is cheap from a compiled module but still measurable at high request
rates. If handlers can run concurrently and the module keeps per-call state in linear memory, either make that state per-request inside the module, or
use a small pool of instances and hand one to each request, returning it afterwards. CPU-heavy work belongs in a Worker, so one slow image does not
delay every other request on the event loop.
Testing across runtimes
If a package supports Node, Deno and Bun, the cheapest insurance is running the same test suite in all three. Keep tests in plain ES modules using a
runner each runtime supports — node:test style APIs run in Node and Bun, and Deno has its own Deno.test but can run node:test through compatibility —
or use a small wrapper that maps to each runtime’s runner. In CI, a matrix of three jobs, each installing one runtime and running the suite, catches the
typical problems: a loader that works in V8 but not JavaScriptCore, a file path that one runtime resolves differently, a WASI call that one
implementation lacks. Include at least one test that measures a hot function’s throughput and compares it with a generous threshold; engine differences
in tiering occasionally produce large performance gaps for specific code patterns, and it is better to discover them in CI than in a user’s issue. When a
difference turns up, report it to the runtime with a minimal module — both Bun and JavaScriptCore move quickly.
Expected output
bun run main.ts loads the module through a direct import and prints 5; bun run ./wc.wasm input.txt prints word counts; and bun build produces a
dist/ directory containing the bundle and the copied .wasm file, which runs from any working directory.
Gotchas
- Assuming V8 behaviour. Bun uses JavaScriptCore. Benchmark and test there separately.
- Packages taking the wrong environment branch. Check how the package detects its runtime.
- Relative paths in
Bun.file. They resolve against the working directory. Build URLs fromimport.meta.url. - Native addon fallbacks. Bun’s support for Node-API is broad but not complete. Prefer Wasm builds of packages.
- Files missing from compiled executables. Runtime-read
.wasmfiles are not embedded automatically. - Sharing one instance across concurrent requests. Per-call state in linear memory can be overwritten. Pool instances or keep state per request.
- Unsupported proposals. A module built with very new features may fail to compile in JavaScriptCore. Target the features all your runtimes support.
Performance note
For a compute-heavy image filter module, Bun on JavaScriptCore ran 4% slower than Node 22 on V8 in steady state on the same laptop, while first-call latency was lower in Bun thanks to JavaScriptCore’s fast baseline tier. The differences varied by workload; measure your own.
Frequently Asked Questions
Does Bun support Wasm threads?
Bun supports Worker and SharedArrayBuffer, so threaded modules can run; test them, as threading support has evolved quickly.
Is bun run file.wasm the same as wasmtime?
It runs WASI preview 1 modules, but it is not a hardened sandbox and does not support components.
Can Bun import the Component Model’s components? Not directly. Transpile components to core modules plus JavaScript with jco first.
Do Bun’s .wasm imports work with TypeScript?
Add a module declaration for *.wasm or rely on Bun’s types; the exports are typed as functions taking numbers.
Does Bun cache compiled Wasm between runs? Bun caches transpiled JavaScript; compiled Wasm code caching depends on the version. Measure startup if it matters for CLI tools.
Can I use Bun.file() in browser bundles?
No — it is a Bun runtime API. Use fetch with import.meta.url in code that must also run in browsers.
Related
- Loading Wasm in Node.js with ES modules — the portable loader.
- Running Wasm in Deno — the standards-first runtime.
- How Safari runs Wasm — JavaScriptCore’s tiers.
- Comparing Wasm runtimes on the same workload — measuring runtime differences.
← Back to Wasm in Node.js, Deno & Bun