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
.wasmmodule whose imports you provide from JavaScript. - [ ] Familiarity with
WebAssembly.ModuleandWebAssembly.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.
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.
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 sourcereturns 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 inscript-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.
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.
Related
- Resolving Wasm URLs with import.meta.url — the dynamic fallback.
- Code-splitting several Wasm modules in one app — loading only what a route needs.
- Loading Wasm in a Web Worker with ESM — workers and modules.
- Streaming instantiation vs ArrayBuffer instantiation — compile paths compared.
← Back to ESM Bindings & Module Generation