Wasm in Extensions & Desktop Apps

Not every WebAssembly module ships on a website. The same modules increasingly run inside browser extensions, Electron and Tauri desktop apps, VS Code extensions, and mobile apps built on webviews with Capacitor or Cordova. Each of these hosts embeds a WebAssembly engine — Chromium’s V8 in Electron, WebView2 and Android’s System WebView, WebKit’s JavaScriptCore in WKWebView and Safari extensions — so modules run with the same performance as in a browser. What changes from host to host is everything around execution: which process or context may compile code, how files are packaged and found, which content security policy applies, what memory the host allows, and which operating system features sit next to the module.

For many teams these hosts are where WebAssembly pays off most. A desktop app that replaces native Node modules with Wasm stops rebuilding binaries for every operating system and Electron version. A VS Code extension built on a Wasm module works on vscode.dev, where native binaries cannot run. A product with a web, desktop and mobile version can share one core module instead of three implementations. This topic covers each host, the constraints it adds, and the patterns that make modules portable across all of them without per-host forks.

Prerequisites

  • [ ] A working WebAssembly module and loader for the web (any toolchain).
  • [ ] The host’s development tools: Chrome or Firefox for extensions, Electron or Tauri, VS Code, Xcode and Android Studio for mobile.
  • [ ] Familiarity with each host’s security model, or willingness to read the relevant guide below first.

The hosts and where modules run

Every host has more than one execution context, and choosing the right one is the first design decision. In browser extensions, modules can run in the background service worker, in offscreen documents, in extension pages and in content scripts — each with different lifetimes and policies. Electron has a Node main process, sandboxed Chromium renderers and windowless utility processes. Tauri pairs a native Rust backend with a platform webview. VS Code runs extensions in a Node extension host on desktop and in a Web Worker on the web. Capacitor apps run the web build in WKWebView or Android’s webview with a native shell around it.

Execution contexts for Wasm in each host Browser extensions run modules in the service worker, offscreen documents or extension pages. Electron uses renderers, the main process or utility processes. Tauri uses the webview, with native Rust commands as the alternative. VS Code uses the desktop Node host or the web worker host. Capacitor uses the platform webview. host where Wasm runs notable constraint MV3 extension service worker / offscreen doc worker may stop anytime Electron renderer / utility process keep main process free Tauri webview (or native Rust) webview differs per OS VS Code extension Node host / web worker host no native code on web Capacitor / Cordova WKWebView / Android WebView phone memory limits

Loading modules from a packaged app

On the web, modules come from a server with HTTP headers. In packaged hosts they come from inside the package, served by the host’s own mechanism — an extension URL from chrome.runtime.getURL, Electron’s custom protocols or process.resourcesPath, Tauri’s asset protocol and resource resolver, VS Code’s extensionUri and workspace.fs, Capacitor’s local scheme. Two failure modes are common across all of them: paths that work in development but not in the packaged build, and local servers or protocols that do not send application/wasm, disabling streaming compilation. The cure is the same everywhere: resolve module locations through the host’s API rather than hard-coded relative paths, check the response type once in each host, and test the packaged build, not just the development one.

From build output to a running module in a packaged host The build produces the .wasm file. The packager includes it as a resource. At runtime the code resolves the module through the host's API, the host serves or reads it, and the module is compiled with streaming where the host sends the right type, or from bytes otherwise. build .wasm release + wasm-opt package as resource extension, app, vsix resolve via host API getURL, resourcesPath … host serves bytes check Content-Type compile + instantiate cache per context

Security policies in packaged hosts

Packaged hosts usually enforce stricter policies than websites, and WebAssembly compilation falls under them. Manifest V3 extensions forbid 'unsafe-eval' and remote code; modules must be bundled and the extension’s CSP must include 'wasm-unsafe-eval'. Electron apps should keep renderers sandboxed with context isolation, which means modules in renderers get only web APIs and whatever a narrow preload bridge exposes. Tauri restricts which commands the front end may invoke, which also bounds what a module in the webview can reach. Mobile app stores restrict downloading executable code after review. In every case, the safe pattern is to bundle modules with the app, give them minimal imports, verify the integrity of anything downloaded later, and run untrusted modules — plugins, user scripts — in separate contexts with no privileged capabilities.

Replacing native code with WebAssembly

A recurring reason to use WebAssembly in desktop and extension hosts is replacing native binaries. Native Node modules in Electron must be rebuilt for every Electron ABI, operating system and CPU architecture; native helper executables in VS Code extensions need a package per platform and cannot run on the web at all; native libraries in mobile apps need separate iOS and Android builds. A single .wasm file replaces all of these, at a modest cost in speed — often 10–40% slower than native for compute — and with the need to move file and network access into the host’s JavaScript. For many products the reduction in build complexity, crash reports after platform updates, and support burden is worth far more than the speed difference.

Platform-specific artefacts per release before and after moving to Wasm Number of platform-specific binary artefacts a product built for each release when its compiled components were native: Electron native modules, VS Code extension helper binaries and mobile native libraries, compared with a single Wasm module used by every host. artefacts per release Electron native modules 9 VS Code platform packages 6 mobile native libraries 4 one Wasm module (all hosts) 1

Sharing one core across hosts

Products that exist in several of these hosts benefit most from WebAssembly when the core logic is shared. The design that works is a core module that only computes, and talks to the outside world through a narrow host interface — storage, time, randomness, logging — implemented by a small adapter in each shell. File paths, network requests, threads and UI stay in the shells. The core is tested once natively with a mock host, and a shared integration suite runs against every adapter. That arrangement keeps behaviour identical across the website, the desktop app, the extension and the mobile app, which is often the real goal rather than speed.

Performance and resource limits

The engines are the same as in browsers, so compute performance matches the corresponding browser. Resource limits differ. Extension service workers stop after short idle periods, so modules must be re-instantiated on demand and must not hold the only copy of important state. Electron’s main process should never run long Wasm calls, because every window depends on it. Mobile webviews have tight memory limits and terminate pages that exceed them, which requires memory budgets and chunked processing. VS Code’s extension host is shared by every extension, so long calls belong in workers. Startup time matters in all of them: compile modules lazily, cache compiled code where the host allows, and measure cold starts on the slowest supported device.

Testing and debugging across hosts

Each host has its own debugging entry point — the extension’s service-worker DevTools, Electron’s DevTools for each process, Safari’s Web Inspector for WKWebView, chrome://inspect for Android webviews, the extension host’s developer tools in VS Code — and each supports WebAssembly debugging comparable to the matching browser. The most valuable automated test is a smoke test against the packaged artefact in each host: install the extension, launch the built desktop app with Playwright, run the .vsix with @vscode/test-web and @vscode/test-electron, and run the mobile app on a device farm or emulator. Such tests catch missing resources, wrong paths, policy problems and feature gaps that unit tests never see.

Choosing between native code and WebAssembly in a host

Hosts with a native side — Electron’s main process, Tauri’s Rust backend, Capacitor plugins, VS Code’s desktop extension host — always offer an alternative to WebAssembly: run the logic natively. The choice follows a few questions. Must the feature run in a browser too, or in VS Code on the web? Then WebAssembly is required somewhere, and sharing the same module is simplest. Is the work called very often from the UI with data already in the web layer? Then WebAssembly next to the UI avoids inter-process round trips. Does it need operating-system access, background execution, large memory or native threads? Then native code fits better. Many products end up with both: a shared core compiled to WebAssembly for portability, and native code for the few things only native code can do, each placed where its strengths matter.

Updates and versioning across hosts

Each host updates on its own schedule. Websites update instantly; browser extensions update through stores within hours to days; desktop apps update when users accept an update, sometimes weeks later; mobile apps wait for store review and user action. When a module and its data formats are shared across those hosts, different versions will meet the same data. Version the module’s interface and its data formats, migrate old data on load, refuse data from newer versions with a clear message, and record the module version in error reports from every host. A shared changelog that calls out format changes helps teams with slow release cycles plan upgrades.

Size budgets per host

Module size affects each host differently. On the web it is download time on every cache miss. In extensions it is install size and update size. In desktop installers it is a small fraction next to the runtime itself, so larger modules are acceptable — though updates multiply their cost. In mobile apps it counts towards app size limits and download warnings on cellular connections. Keep a single size budget for the shared core, track it in CI, and load optional modules — language packs, models, rarely used features — on demand where the host allows downloads, or bundle them only in hosts where size matters little.

Offline and restricted environments

Packaged hosts often run where web pages cannot: offline, behind corporate firewalls, or in managed environments that block downloads. That is a strength — bundled modules work without any network — and a constraint, because features that rely on downloading models or updates must degrade gracefully. Design the module set so the bundled modules provide a complete baseline, and treat downloads as enhancements. Enterprise environments may also run antivirus software that scans new files, and policies that forbid writing executable content to user directories; keeping modules inside the signed installation and caches in standard per-user locations avoids most of those conflicts.

Running untrusted modules inside apps

Desktop and extension hosts are also where third-party WebAssembly most often runs: editor plugins, user scripts, automation steps, community content. The sandbox protects memory, but the imports decide what a module can do, and in these hosts imports can reach powerful APIs — the file system in Electron, privileged extension APIs, Tauri commands. Run untrusted modules in a separate context with no access to those APIs: a utility process in Electron with only the capabilities the plugin contract needs, a sandboxed iframe or worker in a webview, or a dedicated runtime with CPU and memory limits. Validate every message coming back, treat plugin output as untrusted data, and verify signatures for plugins distributed through a registry. The patterns are covered in depth in building a plugin system in the browser.

Observability in packaged hosts

Packaged apps are harder to observe than websites: errors happen on users’ machines, offline, in versions that may be months old. Report module load failures, traps and panics with the host type, host version and module version, and upload debug files for each module release so stack traces can be symbolicated. Respect users’ privacy settings and offline operation — queue reports and send them when allowed. Crash reports from desktop apps often reveal platform-specific issues, such as a particular Linux webview version lacking a feature, that web telemetry never shows.

Developer workflow across hosts

Supporting several hosts multiplies build and test configurations, so a deliberate workflow helps. Build the module once per commit with pinned toolchains and publish it as an internal artefact that every host’s build consumes, rather than each host compiling it separately with possibly different flags. Develop features against the web build first, where reload cycles are fastest, then verify them in each host. Keep a small matrix of smoke tests — one per host, run against packaged builds — and run them on release branches. When a bug appears in only one host, the shared module is rarely at fault; look at that host’s loading path, policy, memory limits or adapter first.

Gotchas and failure modes

  • Development-only paths. Modules found from the project tree vanish in packaged builds. Resolve through host APIs.
  • Missing 'wasm-unsafe-eval'. Strict host policies block compilation. Add the keyword, not 'unsafe-eval'.
  • Long calls in shared contexts. Electron’s main process, the VS Code extension host and the webview main thread freeze. Use workers.
  • Assuming long-lived contexts. Extension service workers stop. Re-instantiate lazily and persist state.
  • Desktop memory assumptions on phones. Mobile webviews terminate heavy pages. Budget and chunk.
  • Testing only in the browser. Each host has its own loading path and policy. Smoke-test packaged builds.
  • Downloading unverified modules. Bundled modules are signed with the app; downloaded ones need your own verification.

Verification

For each host, verify three things on the packaged build: the module loads with the expected response type or through the expected file path; the feature works end to end, including after the host suspends and resumes the context (service-worker restarts, app backgrounding); and resource use — startup time, peak memory — is within budget on the slowest supported device. Automate the first two in CI; check the third on real hardware before releases.

Guides in this topic

Frequently Asked Questions

Is WebAssembly in Electron or Tauri slower than in Chrome? No — Electron uses the same V8; Tauri uses the platform webview, which matches the corresponding browser engine.

Can extensions download new Wasm modules at runtime? Manifest V3 forbids remotely hosted code; bundle modules with the extension and update through the store.

Do I need different builds for different hosts? Usually one module works everywhere; feature-specific builds (threads, SIMD) are chosen by capability, not by host.

Which host is most restrictive? Mobile webviews on memory, MV3 extensions on code loading and lifetimes, and VS Code’s web host on native capabilities.

Can a module call native APIs directly? Never directly — only through imports the host provides, which is what makes the same module safe and portable.

Where should I start for a product with web and desktop versions? With the shared-core pattern: extract host-agnostic logic into a module with a narrow host interface, then add adapters per shell.

Can the same module run in a Safari Web Extension? Yes — Safari’s extension runtime supports WebAssembly with policies similar to Chrome’s MV3; test service-worker lifetimes there separately.

Do these hosts support WASI? Node-based hosts do through node:wasi, and VS Code provides its own WASI implementation; webviews need a JavaScript WASI shim.

Is the same .wasm file usable in an extension and a desktop app? Yes — the binary is portable; only the loader and the security policy around it differ between hosts.

Do app stores review WebAssembly differently from JavaScript? Reviewers mainly check that code is packaged rather than downloaded at run time, so ship modules inside the bundle.

What is the most common packaging failure? A missing MIME type or file path in the packaged host, which breaks streaming compilation; test the packaged build, not only the dev server.

Should each host get its own build of the module? Usually not — one build with host-specific loaders is easier to test, unless a host lacks a feature such as threads or SIMD.

← Back to Production Wasm: Workloads & Deployment