Guide for integrating WebAssembly modules into browser extensions using wasm-pack, wasm-bindgen, and cross-browser loading patterns.
Integrates WebAssembly modules into browser extensions using wasm-pack and cross-browser loading patterns.
npx claudepluginhub arustydev/aiThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Guide for integrating WebAssembly modules into browser extensions using wasm-pack, wasm-bindgen, and cross-browser loading patterns.
WASM in browser extensions enables:
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Build for web target
wasm-pack build --target web --out-dir pkg
# Build for bundler (if using webpack/vite)
wasm-pack build --target bundler --out-dir pkg
[package]
name = "extension-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }
[profile.release]
opt-level = "s" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit for better optimization
strip = true # Strip debug symbols
| Target | Output | Use Case |
|---|---|---|
web | ES modules | Direct browser loading |
bundler | npm package | Webpack/Vite bundling |
nodejs | CommonJS | Node.js (not for extensions) |
no-modules | Global | Legacy browser support |
extension/
├── wasm/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── entrypoints/
│ └── background.ts
└── wxt.config.ts
// wxt.config.ts
import { defineConfig } from 'wxt';
export default defineConfig({
vite: () => ({
plugins: [
// WASM plugin for Vite
{
name: 'wasm-pack',
buildStart: async () => {
const { execSync } = await import('child_process');
execSync('wasm-pack build wasm --target web --out-dir ../public/wasm', {
stdio: 'inherit'
});
}
}
],
build: {
target: 'esnext',
rollupOptions: {
output: {
// Preserve WASM imports
inlineDynamicImports: false
}
}
},
optimizeDeps: {
exclude: ['*.wasm']
}
})
});
// For modules < 4KB
async function loadWasmSync(wasmPath: string): Promise<WebAssembly.Instance> {
const response = await fetch(chrome.runtime.getURL(wasmPath));
const bytes = await response.arrayBuffer();
const module = new WebAssembly.Module(bytes);
return new WebAssembly.Instance(module);
}
// For modules >= 4KB (required by browsers)
async function loadWasmAsync(wasmPath: string): Promise<WebAssembly.Instance> {
const response = await fetch(chrome.runtime.getURL(wasmPath));
if (WebAssembly.instantiateStreaming) {
// Chrome, Firefox, Edge - streaming compilation
const { instance } = await WebAssembly.instantiateStreaming(response);
return instance;
} else {
// Safari fallback
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
return instance;
}
}
// Generated by wasm-pack
import init, { process_data } from './pkg/extension_wasm.js';
let wasmReady = false;
async function initWasm(): Promise<void> {
if (wasmReady) return;
const wasmUrl = chrome.runtime.getURL('pkg/extension_wasm_bg.wasm');
await init(wasmUrl);
wasmReady = true;
}
// Usage
async function processWithWasm(data: Uint8Array): Promise<Uint8Array> {
await initWasm();
return process_data(data);
}
function checkWasmSupport(): {
basic: boolean;
streaming: boolean;
threads: boolean;
simd: boolean;
} {
const basic = typeof WebAssembly !== 'undefined';
const streaming = basic &&
typeof WebAssembly.instantiateStreaming === 'function';
// Check for threads (SharedArrayBuffer)
const threads = basic &&
typeof SharedArrayBuffer !== 'undefined';
// Check for SIMD
const simd = basic && (() => {
try {
// Minimal SIMD validation bytes
const bytes = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123,
3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11
]);
return WebAssembly.validate(bytes);
} catch {
return false;
}
})();
return { basic, streaming, threads, simd };
}
Safari has stricter WASM policies:
// Safari requires explicit WASM MIME type
// Ensure server sends: Content-Type: application/wasm
// Safari doesn't support streaming compilation from cross-origin
// Keep WASM files in extension package
// Safari may require web_accessible_resources for WASM
{
"web_accessible_resources": [
{
"resources": ["pkg/*.wasm", "pkg/*.js"],
"matches": ["<all_urls>"]
}
]
}
| Context | Chrome | Firefox | Safari |
|---|---|---|---|
| Service worker | 128MB default | 512MB | 128MB |
| Content script | Tab memory | Tab memory | Tab memory |
| Popup | 128MB | 128MB | 128MB |
// lib.rs - Minimize allocations
use wasm_bindgen::prelude::*;
// Reuse allocations
static mut BUFFER: Vec<u8> = Vec::new();
#[wasm_bindgen]
pub fn process_chunk(data: &[u8]) -> Vec<u8> {
unsafe {
BUFFER.clear();
BUFFER.extend_from_slice(data);
// Process in place
BUFFER.clone()
}
}
// Free memory explicitly
#[wasm_bindgen]
pub fn free_buffer() {
unsafe {
BUFFER = Vec::new();
BUFFER.shrink_to_fit();
}
}
// Process large data in chunks to avoid memory spikes
async function processLargeData(
data: ArrayBuffer,
chunkSize: number = 1024 * 1024 // 1MB chunks
): Promise<ArrayBuffer> {
await initWasm();
const input = new Uint8Array(data);
const results: Uint8Array[] = [];
for (let i = 0; i < input.length; i += chunkSize) {
const chunk = input.slice(i, i + chunkSize);
const processed = process_chunk(chunk);
results.push(processed);
}
// Combine results
const totalLength = results.reduce((sum, arr) => sum + arr.length, 0);
const output = new Uint8Array(totalLength);
let offset = 0;
for (const result of results) {
output.set(result, offset);
offset += result.length;
}
// Free WASM memory
free_buffer();
return output.buffer;
}
// background.ts (service worker)
let wasmModule: WebAssembly.Module | null = null;
// Pre-compile on install
chrome.runtime.onInstalled.addListener(async () => {
const response = await fetch(chrome.runtime.getURL('pkg/module.wasm'));
const bytes = await response.arrayBuffer();
wasmModule = await WebAssembly.compile(bytes);
// Store compiled module reference for quick instantiation
console.log('WASM module compiled');
});
// Instantiate when needed
async function getWasmInstance(): Promise<WebAssembly.Instance> {
if (!wasmModule) {
const response = await fetch(chrome.runtime.getURL('pkg/module.wasm'));
const bytes = await response.arrayBuffer();
wasmModule = await WebAssembly.compile(bytes);
}
return new WebAssembly.Instance(wasmModule);
}
// Handle service worker termination
// WASM modules are lost when worker sleeps
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PROCESS_WASM') {
// Re-initialize WASM if needed
processWithWasm(message.data)
.then(sendResponse)
.catch(error => sendResponse({ error: error.message }));
return true;
}
});
The plugin includes a wasm-bindgen MCP server for build automation.
{
"mcpServers": {
"wasm-bindgen": {
"command": "cargo",
"args": [
"run",
"--manifest-path",
"${HOME}/.local/share/mcp/wasm-bindgen-mcp/Cargo.toml",
"--",
"--target-dir",
".output/wasm"
],
"env": {
"WASM_PACK_PATH": "wasm-pack",
"WASM_TARGET": "web",
"WASM_EXTENSION_MODE": "true"
}
}
}
}
| Tool | Description |
|---|---|
wasm_build | Build WASM module with wasm-pack |
wasm_optimize | Optimize WASM binary size |
wasm_validate | Validate WASM module |
wasm_inspect | Inspect WASM module exports |
// AI can invoke:
// wasm_build({ profile: 'release', target: 'web' })
// wasm_optimize({ input: 'module.wasm', level: 's' })
// wasm_validate({ path: 'module.wasm' })
# Cargo.toml
[profile.release]
opt-level = "z" # Optimize for size (aggressive)
lto = "fat" # Full LTO
codegen-units = 1
panic = "abort" # No unwinding
strip = true
# Install wasm-opt (from binaryen)
brew install binaryen
# Optimize WASM binary
wasm-opt -Os -o output.wasm input.wasm
# Or with wasm-pack
wasm-pack build --release -- --features wee_alloc
[dependencies]
wee_alloc = { version = "0.4", optional = true }
[features]
default = ["wee_alloc"]
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
use wasm_bindgen::prelude::*;
use sha2::{Sha256, Digest};
#[wasm_bindgen]
pub fn hash_sha256(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
use wasm_bindgen::prelude::*;
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
#[wasm_bindgen]
pub fn compress_gzip(data: &[u8]) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data).unwrap();
encoder.finish().unwrap()
}
use wasm_bindgen::prelude::*;
use serde_json::Value;
#[wasm_bindgen]
pub fn parse_json(input: &str) -> JsValue {
match serde_json::from_str::<Value>(input) {
Ok(value) => serde_wasm_bindgen::to_value(&value).unwrap(),
Err(e) => JsValue::from_str(&format!("Error: {}", e))
}
}
# Build with debug info
wasm-pack build --dev
# Or with custom flags
RUSTFLAGS="-C debuginfo=2" wasm-pack build
use web_sys::console;
#[wasm_bindgen]
pub fn debug_log(message: &str) {
console::log_1(&message.into());
}