Using Wasm in a Manifest V3 Browser Extension
This page answers one task: a browser extension needs WebAssembly — for parsing, image processing, cryptography or a bundled library — and it must work under Manifest V3’s stricter rules, where remote code is forbidden, background pages are service workers, and the content security policy is locked down.
Prerequisites
- [ ] A Manifest V3 extension project with a bundler (Vite, webpack, esbuild) or plain files.
- [ ] A
.wasmmodule and its loader (wasm-bindgenwebtarget, Emscripten ES module, or hand-written). - [ ] Chrome for development; Firefox and Safari if you ship there too.
What Manifest V3 changes for WebAssembly
Manifest V3 tightened three things that matter here. First, extensions may not execute remotely hosted code: every .wasm file must be packaged inside
the extension, and fetching a module from a server to compile it is disallowed by store policies. Second, extension pages run under a mandatory content
security policy that forbids 'unsafe-eval'; WebAssembly compilation needs the narrower 'wasm-unsafe-eval' keyword, which MV3 permits. Third, the
background page became a service worker that the browser starts and stops on demand, so a module instantiated in the background can disappear between
events, along with its memory.
Content scripts add one more wrinkle: they run inside web pages, and depending on the browser their WebAssembly compilation may be subject to the host page’s policy. The robust pattern is to do Wasm work in extension contexts you control — the service worker or an offscreen document — and message results to content scripts.
Step 1 — allow compilation in the manifest
{
"manifest_version": 3,
"name": "Wasm Helper",
"version": "1.0.0",
"background": { "service_worker": "background.js", "type": "module" },
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"web_accessible_resources": [
{ "resources": ["wasm/*.wasm"], "matches": ["<all_urls>"] }
]
}
'wasm-unsafe-eval' allows compiling WebAssembly without enabling JavaScript eval, which MV3 forbids. List .wasm files in web_accessible_resources
only if content scripts must fetch them directly; for service-worker or offscreen use, they can stay private. The CSP keyword itself is explained in
fixing CSP errors that block Wasm compilation.
Step 2 — load the module in the service worker
Load from the extension’s own URL with chrome.runtime.getURL, and initialise lazily so a restarted worker re-instantiates on demand:
// background.js
import init, { analyze } from "./wasm/analyzer.js";
let ready;
function ensureWasm() {
ready ??= init({ module_or_path: chrome.runtime.getURL("wasm/analyzer_bg.wasm") });
return ready;
}
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type !== "analyze") return;
ensureWasm().then(() => sendResponse({ result: analyze(msg.text) }));
return true; // keep the channel open for the async response
});
Because the worker can be stopped after about 30 seconds of inactivity, never assume the module is still loaded; the lazy ensureWasm pattern handles
restarts. Compilation is cached by the browser, so re-instantiation after a restart is usually fast. Do not store important state only in linear memory —
persist it with chrome.storage or IndexedDB.
Step 3 — use an offscreen document for heavy or DOM-dependent work
Service workers have no DOM, no OffscreenCanvas in every browser, and a short lifetime. For long computations, canvas-based image work, audio, or
modules that use workers and shared memory, create an offscreen document:
await chrome.offscreen.createDocument({
url: "offscreen.html",
reasons: ["WORKERS"],
justification: "Run the Wasm image processor with a worker pool",
});
const out = await chrome.runtime.sendMessage({ target: "offscreen", type: "resize", bytes });
The offscreen page loads the module like any extension page and stays alive while it has work. Chrome requires a reason from a fixed list; Firefox’s background pages can be event pages with similar capabilities instead.
Step 4 — be careful in content scripts
If a content script must run WebAssembly itself — for latency-sensitive work on page data — load the module from the extension’s URL listed in
web_accessible_resources. Test on pages with strict content security policies: some browsers apply the page’s policy to the content script’s compilation,
which then fails on sites that do not allow 'wasm-unsafe-eval'. Messaging to the service worker avoids the issue at the cost of a round trip and a copy
of the data, which is usually acceptable.
Step 5 — package and test across browsers
Bundle the .wasm files with the extension; bundlers may need configuration to copy them rather than inline them. Chrome Web Store, Firefox Add-ons and
the Safari App Store all review extensions, and reviewers may ask about WebAssembly — include the source or build instructions for the module, since
reviewers must be able to verify that the binary corresponds to source. Test in each browser: Firefox supports MV3 with differences in background scripts,
and Safari’s extension runtime has its own quirks with service-worker lifetimes and file access.
Reproducible builds for store review
Extension stores increasingly ask how binary components were produced. Mozilla’s add-on policies require that reviewers can rebuild any minified or
compiled code from source, and other stores ask similar questions during review. Treat the Wasm build as part of the extension’s source: commit the Rust
or C code, pin the toolchain, and provide a single script or Dockerfile that rebuilds the .wasm byte for byte. Reproducible output, as described in
producing reproducible Wasm binaries,
turns a potentially long review conversation into a quick verification. Keep the module’s imports minimal and document them, which also makes the
security review easier: a parser module that imports nothing beyond memory is easy to reason about.
Size, startup and battery
Extensions load on every browser start and often on every page, so their costs are multiplied across a user’s whole browsing session. Keep the module small with size-optimised builds, and do not instantiate it until it is needed — the lazy pattern in the service worker already does this. Avoid polling or timers that keep the service worker alive just to keep a module in memory; it defeats MV3’s design and drains battery on laptops. If startup latency matters, cache compiled modules in IndexedDB or rely on the browser’s code cache, which works for extension resources as for web pages. Measure memory too: a module that grows linear memory to hundreds of megabytes in a service worker or offscreen document holds it until the context is destroyed.
Passing page data to the module
Content scripts see page data as DOM nodes and strings; the module wants bytes. Extract only what the module needs in the content script — the text of
selected elements, image URLs, a structured summary — and send that, rather than whole documents. Messages between extension contexts are copied with
structured cloning, so large payloads cost time on both sides; ArrayBuffers are cloned too, since extension messaging does not support transfer in all
browsers. For images, send URLs and let the extension context fetch them with its own permissions, which also avoids cross-origin restrictions the page
would impose. Keep messages small and the module’s interface coarse.
Expected output
The extension loads its Wasm analyser in the service worker on first use, re-instantiates transparently after the worker restarts, runs image work in an offscreen document with a worker pool, passes Chrome Web Store review with a reproducible build script, and works on pages with strict CSPs because content scripts delegate Wasm work to extension contexts.
Gotchas
- Fetching modules from a server. Remote code is forbidden in MV3. Package every
.wasm. - Using
'unsafe-eval'. It is not allowed in MV3. Use'wasm-unsafe-eval'. - Assuming the service worker stays alive. Re-initialise lazily and persist state elsewhere.
- Compiling in content scripts on strict pages. Delegate to the service worker or offscreen document.
- No source for reviewers. Provide build instructions and source for the binary.
Performance note
Instantiating a 400 KB module in the service worker took about 25 ms on first use and about 4 ms after a worker restart, thanks to the browser’s code cache. Forwarding a 2 MB image to an offscreen document and back added about 12 ms of messaging overhead.
Frequently Asked Questions
Can I use SharedArrayBuffer in an extension? Extension pages can be cross-origin isolated with the right manifest keys; check each browser’s support before relying on threads.
Does Firefox support 'wasm-unsafe-eval' in extensions?
Yes, in MV3 and MV2 extension pages, with the same meaning.
Can I use wasm-bindgen’s generated glue?
Yes, with the web target and an explicit module path from chrome.runtime.getURL.
How do I debug Wasm in the service worker? Open the service worker’s DevTools from the extensions page; Wasm debugging works as in normal pages.
Do stores reject extensions with WebAssembly? No, but they review it like other code. Provide source and reproducible builds.
Can the extension send large buffers between contexts quickly? Messages are cloned; for large data, have the receiving context fetch it by URL, or keep the work in one context.
Related
- Running Wasm in Electron apps — another host with its own rules.
- Writing a Content Security Policy for Wasm — CSP in depth.
- Loading Wasm in a Web Worker with ESM — worker loading patterns.
- Caching compiled Wasm modules in IndexedDB — faster restarts.
← Back to Wasm in Extensions & Desktop Apps