Loading Wasm with Source-Phase Imports

This page answers one task: you want to load a WebAssembly module with a static import statement — so bundlers and runtimes can see the dependency, preload it and compile it early — while still controlling instantiation yourself, with your own import object and as many instances as you need.

Prerequisites

  • [ ] A runtime or bundler that supports source-phase imports (check current support; it is newer than other ESM features).
  • [ ] A .wasm module whose imports you provide from JavaScript.
  • [ ] Familiarity with WebAssembly.Module and WebAssembly.instantiate.

Two phases of importing Wasm

The ESM integration for WebAssembly lets import { f } from "./m.wasm" load, compile and instantiate a module as if it were a JavaScript module, resolving its imports as other ES modules. That is convenient for self-contained modules but takes control away: you cannot pass a custom import object, choose when to instantiate, or create several instances with different memories.

Source-phase imports split the work at a useful point. The statement import source engine from "./engine.wasm" fetches and compiles the module as part of the module graph — so tooling knows about it, can preload it, and can compile it while other modules load — but gives you the compiled WebAssembly.Module, not an instance. Instantiation stays in your code, with the full flexibility of the JavaScript API. It is the same idea as WebAssembly.compileStreaming, expressed as a static, analysable import.

Ways to bring a Wasm module into JavaScript Fetch and compileStreaming is fully dynamic and invisible to bundlers. A source-phase import compiles the module as part of the module graph and returns a WebAssembly.Module for you to instantiate. A full ESM-integration import also instantiates, resolving imports as ES modules, with less control. fetch + compileStreaming dynamic, runtime URL you instantiate tooling cannot see it works everywhere import source static, in the module graph returns WebAssembly.Module you instantiate control + tooling import { f } from .wasm static, in the module graph engine instantiates imports resolved as ESM self-contained modules

Step 1 — import the module’s source

import source engineModule from "./engine.wasm";

console.log(engineModule instanceof WebAssembly.Module);     // true
console.log(WebAssembly.Module.exports(engineModule));        // [{ name: "checksum", kind: "function" }, …]

The import completes before the importing module runs, like any static import, so engineModule is ready at the top level. Errors in fetching or compiling surface as module-loading errors, the same way a syntax error in an imported JavaScript file would.

Step 2 — instantiate with your own imports

Because you hold the compiled module, instantiation is fully under your control — including several instances with different imports or memories:

const memory = new WebAssembly.Memory({ initial: 32, maximum: 512 });
const instance = await WebAssembly.instantiate(engineModule, {
  env: { memory, log: (ptr, len) => console.log(readString(memory, ptr, len)) },
});
export const checksum = (bytes) => callChecksum(instance.exports, memory, bytes);

Instantiating from a compiled module is fast — no compilation happens here — which makes per-request or per-tenant instances cheap, as described in instantiating one module many times.

Step 3 — share the module with workers

A WebAssembly.Module can be posted to workers without recompilation. With source-phase imports, the main thread gets the compiled module from the module graph and hands it to a worker pool:

import source engineModule from "./engine.wasm";
const worker = new Worker(new URL("./engine.worker.js", import.meta.url), { type: "module" });
worker.postMessage({ type: "init", module: engineModule });

Workers can also import the source themselves; the runtime’s module cache usually avoids compiling twice, but posting the module makes sharing explicit.

A source-phase import from graph to instances The static import puts the .wasm file into the module graph, where it is fetched and compiled before the importing module runs. The importing code receives a WebAssembly.Module and creates instances with its own imports, on the main thread and in workers it posts the module to. import source m static, analysable fetch + compile during module loading WebAssembly. Module ready at top level instantiate(m, imports) your imports, your memory post to workers no recompilation

Step 4 — check support and provide a fallback

Source-phase imports are a recent addition: support has arrived in Node and Deno behind flags or by default in recent versions, and bundlers are adding support, while browser support is still rolling out. Check your targets before depending on it. Until support is universal, keep a dynamic fallback with identical behaviour, or let the bundler compile import source into a compileStreaming call:

// fallback with the same result type
const engineModule = await WebAssembly.compileStreaming(fetch(new URL("./engine.wasm", import.meta.url)));

Writing the loader so that only this one line differs keeps the rest of the code unchanged when you switch.

Step 5 — let bundlers do their part

The main advantage over fetch is visibility. A bundler that understands source-phase imports can copy the .wasm into the output with a content hash, rewrite the path, preload it with <link rel="modulepreload">-style hints, and report it in bundle analysis. A dynamic fetch(new URL(...)) pattern gets some of this through import.meta.url detection, but a dedicated syntax is less fragile. The broader picture of static Wasm imports is in importing Wasm with the ESM integration proposal.

When to choose which form

Choose the full ESM integration for small, self-contained modules whose imports are other JavaScript modules and that need a single instance — utility libraries, pure functions. Choose source-phase imports when you need control: custom import objects, memories you create, several instances, workers, or instantiation deferred until a feature is used. Keep fetch with compileStreaming when the URL is only known at runtime — chosen by feature detection, loaded from a CDN chosen by configuration, or selected per user. Libraries that must work everywhere today may still prefer the fetch form with a new URL(..., import.meta.url) literal, which all major bundlers understand, and adopt source-phase imports as support matures.

Interaction with top-level await and startup

Static imports block the importing module until they finish, which is good for correctness and bad if the module is large and not needed immediately. A source-phase import of a 5 MB module delays everything that depends on the importing file. Keep such imports in modules that are themselves loaded lazily — behind a dynamic import() of the feature’s entry point — so the compiled module arrives only when the feature is used. Conversely, for modules needed at startup, the static form helps: the runtime can start fetching and compiling the .wasm in parallel with other modules as soon as it sees the import, rather than after the importing code runs and calls fetch.

Using it with generated glue

Glue generators expect to load the binary themselves, but most accept a compiled module instead of a URL. wasm-bindgen’s init and initSync take { module_or_path: WebAssembly.Module }, and Emscripten’s factory accepts an instantiateWasm hook that receives imports and must return an instance, into which a source-imported module fits naturally. That combination gives generated bindings the benefits of the static import — early compilation, bundler visibility — without hand-writing the glue:

import source engineModule from "./pkg/engine_bg.wasm";
import { initSync, parse } from "./pkg/engine.js";
initSync({ module: engineModule });

Check the generator’s current API name for the option, which has changed between releases. Libraries that ship generated glue can expose an init(module?) function that accepts an already compiled module, so applications using source-phase imports pass it in while others let the library fetch the file.

Security and content policies

A source-phase import compiles WebAssembly, so it is governed by the same Content Security Policy rule as other compilation: the page needs 'wasm-unsafe-eval' in script-src. The fetch itself follows the module graph’s rules, which means cross-origin imports need CORS and subresource integrity can be applied through import maps’ integrity field where supported. Treat the static import as no more and no less trusted than a static JavaScript import from the same location.

Expected output

import source engineModule from "./engine.wasm" yields a WebAssembly.Module at the top level; the code instantiates it twice with different memories, posts it to a worker without recompiling, and a fallback using compileStreaming provides the same object in runtimes without support.

Gotchas

  • Expecting an instance. import source returns a compiled module. Instantiate it yourself.
  • Assuming universal support. Check runtimes and bundlers; keep a fallback.
  • Large modules in eagerly loaded files. They delay startup. Import them from lazily loaded modules.
  • MIME types still matter. Runtimes fetch the file over HTTP in browsers; serve application/wasm.
  • Mixing with full ESM integration for the same file. Pick one form per module to avoid double loading.
  • CSP without 'wasm-unsafe-eval'. Static imports still compile Wasm. Allow it in script-src.

Performance note

Compiling a 1.8 MB module through a source-phase import overlapped with loading other modules, shortening time to first instance by about 60 ms compared with fetching after the importing module ran. Instantiating from the compiled module took 0.4 ms per instance.

Time to a usable instance by loading form Milliseconds from page start until the first instance of a 1.8 MB module is ready, using fetch and compileStreaming after the importing module runs, and using a source-phase import compiled during module loading. ms to first instance fetch after module runs 210 ms source-phase import 150 ms

Frequently Asked Questions

Is import source the same as import ... with { type: "wasm" }? No. Import attributes describe the resource type; source-phase imports change what the import returns — a compiled module rather than an instance.

Can I use it in TypeScript? TypeScript is adding syntax support; add a declaration for *.wasm source imports or use the fallback until your version supports it.

Does the module compile twice if two files import it? No — module-graph imports are cached by URL, so both receive the same compiled module.

Can I import from a CDN URL? Yes, subject to CORS and the same MIME-type rules as fetch.

Can wasm-bindgen glue use a source-imported module? Yes — pass the compiled module to initSync or init, so the glue skips its own fetch.

Do source phase imports work in Node.js? Check the current release notes; support arrives behind flags first, so keep a fallback loader.

← Back to ESM Bindings & Module Generation