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 code scaffold or equivalent) with TypeScript.
  • [ ] A .wasm module: a library with JavaScript bindings, or a WASI command-line tool.
  • [ ] @vscode/vsce for packaging, and optionally @vscode/test-web for 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.

Native binaries versus Wasm in VS Code extensions A native binary works only in the desktop extension host and needs a package per platform. A Wasm module runs in both the desktop Node host and the web worker host, with one package for every platform, at some cost in speed and a requirement to avoid native-only APIs. native binary desktop extension host only one build per OS and arch fastest execution desktop-only tooling Wasm module desktop and web hosts one package everywhere near-native speed portable tooling

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.

One extension, two hosts The desktop extension host is a Node process and loads the extension's main entry. The web extension host is a worker in the browser and loads the browser entry. Both read the same packaged .wasm file through vscode.workspace.fs and run it, optionally through VS Code's WASI implementation. package.json main + browser entries desktop host (Node) dist/node/extension. js web host (worker) dist/web/extension. js workspace.fs. readFile same .wasm for both run module directly or via WASI

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. Use vscode.workspace.fs.
  • Blocking the extension host. Long calls stall every extension. Use workers.
  • Inlining the .wasm into the bundle. It bloats JavaScript and slows startup. Ship it as a file.
  • Testing only on desktop. Run @vscode/test-web too.
  • 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.

Formatting a 5,000-line file Milliseconds to format the same file with the native formatter binary on desktop, with the Wasm build in the desktop extension host, and with the Wasm build in the web extension host on vscode.dev. ms per format native binary (desktop only) 18 ms Wasm, desktop host 31 ms Wasm, web host 38 ms

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.

← Back to Wasm in Extensions & Desktop Apps