Running Wasm in VS Code Extensions
This page answers one task: a VS Code extension needs a compiled component — a parser, a formatter, a linter, a language tool written in Rust, C or Go — and it must work both in desktop VS Code and in the browser-based editors (vscode.dev, github.dev), where native binaries cannot run at all.
Prerequisites
- [ ] A VS Code extension project (
yo codescaffold or equivalent) with TypeScript. - [ ] A
.wasmmodule: a library with JavaScript bindings, or a WASI command-line tool. - [ ]
@vscode/vscefor packaging, and optionally@vscode/test-webfor testing in the browser host.
Two extension hosts, one module
VS Code runs extensions in an extension host. On desktop it is a Node.js process, so extensions can use fs, child processes and native binaries. In
the web editors, the extension host is a Web Worker in the browser: no file system, no child processes, no native code — only web APIs and WebAssembly.
An extension that shells out to a native formatter works on desktop and does nothing on vscode.dev. An extension built around a Wasm module works in both,
with one binary, no per-platform packages and no installation steps for users.
That makes WebAssembly the natural way to ship compiled tooling in VS Code extensions. Language servers, formatters and linters written in Rust or Go are increasingly distributed this way, and VS Code supports it with documented patterns for loading modules in both hosts and an official WASI implementation for running command-line tools unchanged.
Step 1 — declare both entry points
package.json lists separate entry points for the Node and web hosts; both can share most code:
{
"main": "./dist/node/extension.js",
"browser": "./dist/web/extension.js",
"activationEvents": ["onLanguage:toml"],
"files": ["dist/**", "wasm/*.wasm"]
}
Bundle each entry with esbuild or webpack, targeting Node for main and the browser (WebWorker) for browser. Keep the .wasm file as a separate asset
in the package rather than inlining it, so it is compiled efficiently.
Step 2 — load the module from the extension’s URI
Read the module through VS Code’s file system API, which works in both hosts and resolves files inside the installed extension:
import * as vscode from "vscode";
export async function loadWasm(context: vscode.ExtensionContext) {
const uri = vscode.Uri.joinPath(context.extensionUri, "wasm", "formatter_bg.wasm");
const bytes = await vscode.workspace.fs.readFile(uri); // Uint8Array, desktop and web
const module = await WebAssembly.compile(bytes);
return WebAssembly.instantiate(module, imports);
}
vscode.workspace.fs abstracts over disk on desktop and the extension’s hosted files on the web, so one code path serves both. Cache the compiled
WebAssembly.Module for the lifetime of the extension host; activation should not recompile it on every command.
Step 3 — run WASI tools with VS Code’s WASI support
For tools built as WASI command-line programs — compiled with wasm32-wasip1 from Rust or with wasi-sdk from C — VS Code provides a WASI implementation
through the @vscode/wasm-wasi package (via the “WebAssembly Execution Engine” extension). It maps WASI file access onto workspace files, standard I/O
onto VS Code terminals or pseudo-terminals, and runs the module in a worker, in both hosts:
import { Wasm } from "@vscode/wasm-wasi/v1";
const wasm: Wasm = await Wasm.load();
const pty = wasm.createPseudoterminal();
const terminal = vscode.window.createTerminal({ name: "fmt", pty, isTransient: true });
const module = await WebAssembly.compile(await vscode.workspace.fs.readFile(toolUri));
const process = await wasm.createProcess("fmt", module, {
stdio: pty.stdio,
mountPoints: [{ kind: "workspaceFolder" }],
});
await process.run();
This lets an existing command-line tool run inside the editor on the web with no code changes beyond compiling for WASI. Check the current API version in the package’s documentation, as it has evolved.
Step 4 — keep heavy work off the extension host
The extension host runs every extension. A long synchronous Wasm call blocks other extensions’ events — completions, diagnostics, hovers — for as long as
it runs. Run expensive work in a worker (Node worker_threads on desktop, a Web Worker on the web) or through the WASI process API, which runs modules in
workers already. For language servers, run the server — Wasm or not — in its own worker and talk to it over the language server protocol, as many
Wasm-based language servers do.
Step 5 — package and test in both hosts
vsce package creates a single .vsix containing the module; there is no need for platform-specific packages. Test the web host locally with
@vscode/test-web, which launches a browser with the extension loaded from your build, and run the desktop tests with @vscode/test-electron. Both should
exercise the Wasm path. Watch the package size: the marketplace accepts large packages, but users download them on install and update.
Language servers in Wasm
The most ambitious use is running a whole language server as WebAssembly. A Rust language server compiled for WASI can read workspace files through VS Code’s WASI file-system mapping and communicate over standard input and output, which the language client library can connect to. The same server binary then works on desktop and on vscode.dev, where previously only the syntax-highlighting part of a language extension would have functioned. Startup time and memory matter here, since language servers run for the whole session: keep the binary small, compile it once and cache the compiled module, and measure memory in the web host, where the browser tab’s limits apply. Indexing a large project can be slower than natively; incremental and lazy analysis help more than raw compute speed.
Size and activation time
Extensions are activated on demand, and users notice slow activation. Measure how long it takes to read and compile the module on activation in both hosts — compiling a 3 MB module may take 50–150 ms — and defer it until a command or language feature actually needs it. VS Code’s built-in profiler (Developer: Show Running Extensions) shows activation times per extension. Size-optimised builds and lazy loading keep the extension light for users who install it but use it rarely.
Accessing workspace files from the module
Modules usually need the user’s files. Library-style modules should receive file contents from the extension: read documents with vscode.workspace.fs
or from the open TextDocument, pass the bytes or text into the module, and apply results with WorkspaceEdits. That keeps the module free of any
file-system assumptions and works identically on remote workspaces, virtual file systems and the web, where “files” may live in a GitHub repository
accessed over HTTP. WASI programs get files through the mount points configured when the process is created; mount only the workspace folder they need,
and remember that file access in the web host goes over the network, so tools that scan whole projects are slower there than on desktop. Prefer working
on open documents when the feature allows it.
Expected output
The extension formats TOML files using a Rust formatter compiled to Wasm, in desktop VS Code and on vscode.dev, from a single .vsix; activation compiles the
module once in about 40 ms; and heavy formatting runs in a worker without delaying other extensions.
Gotchas
- Native binaries in web extensions. They cannot run. Ship Wasm for web support.
- Reading the module with
fs. It does not exist in the web host. Usevscode.workspace.fs. - Blocking the extension host. Long calls stall every extension. Use workers.
- Inlining the
.wasminto the bundle. It bloats JavaScript and slows startup. Ship it as a file. - Testing only on desktop. Run
@vscode/test-webtoo. - Compiling on every command. Cache the compiled module for the host’s lifetime.
Performance note
A Rust-based formatter compiled to Wasm formatted a 5,000-line file in 31 ms in the desktop host and 38 ms in the web host, against 18 ms for the native
binary. Packaging one .vsix replaced six platform-specific packages.
Frequently Asked Questions
Do I need the WASI extension for every Wasm module? No — only for WASI programs. Libraries with JavaScript bindings load directly.
Can a web extension use threads? Shared-memory threads depend on the host’s isolation; plain workers are available and usually sufficient.
How do I debug the Wasm part? Use the extension host’s developer tools; for the web host, the browser’s DevTools.
Can I keep a native fast path on desktop? Yes — detect the host and use the native binary on desktop, falling back to Wasm on the web, at the cost of more packaging.
Does the module work with remote and virtual workspaces?
If it receives contents through vscode.workspace.fs or open documents rather than paths, yes.
Will the module work in Cursor, VSCodium and other forks? Generally yes, since they share the extension host; check that forks include the WASI extension if you depend on it.
Related
- Sharing one Wasm core across web and desktop — one module in several shells.
- Running WASI modules in the browser — WASI shims in general.
- Compiling Rust to wasm32-wasip1 — building the tool.
- Running Wasm off the event loop in Node.js — workers in Node.
← Back to Wasm in Extensions & Desktop Apps