Decision framework for WebAssembly usage in browser extensions, including browser-specific loading patterns, architectural patterns, and performance considerations.
Provides a decision framework for using WebAssembly in browser extensions with browser-specific patterns and performance guidance.
npx claudepluginhub arustydev/aiThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Decision framework for WebAssembly usage in browser extensions, including browser-specific loading patterns, architectural patterns, and performance considerations.
| Use Case | Recommendation | Rationale |
|---|---|---|
| Cryptographic operations | Strong yes | 10-100x faster, constant-time |
| Image/video processing | Strong yes | Parallelizable, memory efficient |
| Compression/decompression | Yes | CPU-intensive, existing Rust libs |
| Complex parsing (binary) | Yes | Type safety, predictable perf |
| Scientific computing | Yes | Numerical precision, speed |
| Simple data transforms | No | Overhead exceeds benefit |
| DOM manipulation | No | JS required, no benefit |
| API calls/networking | No | I/O bound, not CPU bound |
| String manipulation | Maybe | Only for large-scale ops |
Score each factor 1-5, then calculate totals:
| Factor | Weight | Score |
|---|---|---|
| CPU intensity | 3x | [1-5] |
| Data volume | 2x | [1-5] |
| Performance criticality | 2x | [1-5] |
| Existing Rust/C++ code | 2x | [1-5] |
| Team Rust expertise | 1x | [1-5] |
Scoring guide:
| Pattern | Problem | Alternative |
|---|---|---|
| WASM for DOM access | Impossible, must call JS | Keep DOM in JS layer |
| WASM for simple logic | Overhead exceeds benefit | Native JS |
| Frequent WASM↔JS calls | Call overhead ~10μs each | Batch operations |
| Large data copies | Memory duplication | Use SharedArrayBuffer |
| Sync WASM in main thread | Blocks UI | Web Worker or async |
// Service worker has no DOM, but full WASM support
let wasmModule: WebAssembly.Module | null = null;
// Pre-compile on install for fast instantiation
chrome.runtime.onInstalled.addListener(async () => {
const response = await fetch(chrome.runtime.getURL('wasm/module.wasm'));
wasmModule = await WebAssembly.compileStreaming(response);
});
// Instantiate per-use (service worker may have terminated)
async function getWasmInstance(): Promise<WebAssembly.Instance> {
if (!wasmModule) {
const response = await fetch(chrome.runtime.getURL('wasm/module.wasm'));
wasmModule = await WebAssembly.compileStreaming(response);
}
return new WebAssembly.Instance(wasmModule);
}
// Firefox supports both event pages and service workers
// Event page approach (MV2) - has DOM access
let wasmInstance: WebAssembly.Instance | null = null;
async function initWasm(): Promise<WebAssembly.Instance> {
if (wasmInstance) return wasmInstance;
const response = await fetch(browser.runtime.getURL('wasm/module.wasm'));
// Firefox supports streaming compilation
const { instance } = await WebAssembly.instantiateStreaming(response);
wasmInstance = instance;
return instance;
}
// Safari has strict WASM policies
// 1. No streaming from cross-origin (must use buffer)
// 2. WASM files must be in extension bundle
// 3. Content-Type must be application/wasm
async function initWasmSafari(): Promise<WebAssembly.Instance> {
const response = await fetch(browser.runtime.getURL('wasm/module.wasm'));
// Safari fallback: no streaming compilation
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
return instance;
}
// Cross-browser wrapper
async function initWasmCrossBrowser(): Promise<WebAssembly.Instance> {
const response = await fetch(browser.runtime.getURL('wasm/module.wasm'));
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
const { instance } = await WebAssembly.instantiateStreaming(response);
return instance;
} catch {
// Fallback on streaming failure
}
}
// Safari and fallback path
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
return instance;
}
// Content scripts run in isolated world
// WASM must be in web_accessible_resources
async function loadWasmInContentScript(): Promise<WebAssembly.Instance> {
// Use chrome.runtime.getURL for proper extension URL
const wasmUrl = chrome.runtime.getURL('wasm/module.wasm');
const response = await fetch(wasmUrl, {
credentials: 'omit', // Don't send cookies
cache: 'force-cache' // Cache aggressively
});
if (!response.ok) {
throw new Error(`Failed to load WASM: ${response.status}`);
}
return initWasmCrossBrowser();
}
WASM runs only in service worker, content scripts communicate via messaging.
┌─────────────────┐ message ┌──────────────────┐
│ Content Script │◄───────────────►│ Service Worker │
│ (no WASM) │ │ (WASM loaded) │
└─────────────────┘ └──────────────────┘
Pros: Single WASM instance, simpler memory management
Cons: Message serialization overhead, latency
Best for: Infrequent operations, small data
// content-script.ts
async function processData(data: Uint8Array): Promise<Uint8Array> {
const response = await chrome.runtime.sendMessage({
type: 'WASM_PROCESS',
data: Array.from(data) // Must serialize
});
return new Uint8Array(response.result);
}
// background.ts
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'WASM_PROCESS') {
processWithWasm(new Uint8Array(msg.data))
.then(result => sendResponse({ result: Array.from(result) }));
return true;
}
});
WASM runs in each content script instance.
┌────────────────────────────────────────────────────────┐
│ Tab 1 │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ Content Script │────►│ WASM (per-tab instance) │ │
│ └─────────────────┘ └──────────────────────────┘ │
└────────────────────────────────────────────────────────┘
Pros: No message latency, parallel processing
Cons: Memory per tab, startup per tab
Best for: Per-page processing, large data
// content-script.ts
import init, { process } from './wasm/module';
let wasmReady = false;
async function ensureWasm(): Promise<void> {
if (wasmReady) return;
await init(chrome.runtime.getURL('wasm/module_bg.wasm'));
wasmReady = true;
}
async function processLocally(data: Uint8Array): Promise<Uint8Array> {
await ensureWasm();
return process(data);
}
WASM runs in dedicated worker, avoiding main thread blocking.
┌─────────────────┐ postMessage ┌─────────────────┐
│ Content Script │◄──────────────────►│ Web Worker │
│ (main thread) │ │ (WASM loaded) │
└─────────────────┘ └─────────────────┘
Pros: Non-blocking, parallelizable
Cons: Setup complexity, message overhead
Best for: Heavy computation, real-time processing
// wasm-worker.ts
import init, { process } from './wasm/module';
self.onmessage = async (e) => {
await init();
const result = process(e.data.input);
// Transfer buffer ownership (zero-copy)
self.postMessage(
{ result: result.buffer },
[result.buffer]
);
};
// content-script.ts
const worker = new Worker(chrome.runtime.getURL('wasm-worker.js'));
function processAsync(data: Uint8Array): Promise<Uint8Array> {
return new Promise((resolve) => {
worker.onmessage = (e) => resolve(new Uint8Array(e.data.result));
worker.postMessage(
{ input: data.buffer },
[data.buffer] // Transfer ownership
);
});
}
Use offscreen document for DOM-dependent WASM operations.
// background.ts
async function ensureOffscreen(): Promise<void> {
if (await chrome.offscreen.hasDocument()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER', 'WORKERS'],
justification: 'WASM processing with DOM'
});
}
async function processViaOffscreen(data: Uint8Array): Promise<Uint8Array> {
await ensureOffscreen();
const response = await chrome.runtime.sendMessage({
target: 'offscreen',
type: 'WASM_PROCESS',
data: Array.from(data)
});
return new Uint8Array(response.result);
}
| Operation | JS (ms) | WASM (ms) | Speedup |
|---|---|---|---|
| SHA-256 (1MB) | 45 | 8 | 5.6x |
| AES-256 encrypt | 120 | 15 | 8x |
| JSON parse (10MB) | 150 | 40 | 3.75x |
| GZIP compress | 200 | 35 | 5.7x |
| Image resize | 300 | 50 | 6x |
| Regex (complex) | 80 | 25 | 3.2x |
| Simple sum | 0.1 | 0.2 | 0.5x (slower!) |
| Scenario | Why JS Wins |
|---|---|
| <1ms operations | WASM call overhead dominates |
| Single string ops | JS strings optimized |
| DOM manipulation | Must call JS anyway |
| Small arrays (<1KB) | Copy overhead dominates |
| JIT-optimized hot paths | V8/SpiderMonkey excellent |
async function benchmarkOperation(
jsImpl: (data: Uint8Array) => Uint8Array,
wasmImpl: (data: Uint8Array) => Uint8Array,
data: Uint8Array,
iterations: number = 100
): Promise<{ js: number; wasm: number; speedup: number }> {
// Warm up
jsImpl(data);
wasmImpl(data);
// Measure JS
const jsStart = performance.now();
for (let i = 0; i < iterations; i++) {
jsImpl(data);
}
const jsTime = (performance.now() - jsStart) / iterations;
// Measure WASM
const wasmStart = performance.now();
for (let i = 0; i < iterations; i++) {
wasmImpl(data);
}
const wasmTime = (performance.now() - wasmStart) / iterations;
return {
js: jsTime,
wasm: wasmTime,
speedup: jsTime / wasmTime
};
}
| Context | Chrome | Firefox | Safari |
|---|---|---|---|
| Service Worker | 128MB | 512MB | 128MB |
| Event Page | N/A | 512MB | N/A |
| Content Script | Tab limit | Tab limit | Tab limit |
| Popup | 128MB | 128MB | 128MB |
// Rust: Reuse buffers to avoid allocation
static mut BUFFER: Vec<u8> = Vec::new();
#[wasm_bindgen]
pub fn process_reuse(data: &[u8]) -> *const u8 {
unsafe {
BUFFER.clear();
BUFFER.extend_from_slice(data);
// Process BUFFER...
BUFFER.as_ptr()
}
}
// Return length separately
#[wasm_bindgen]
pub fn get_result_len() -> usize {
unsafe { BUFFER.len() }
}
// TypeScript: Read result without copy
const ptr = wasmInstance.exports.process_reuse(inputPtr);
const len = wasmInstance.exports.get_result_len();
const memory = new Uint8Array(wasmInstance.exports.memory.buffer);
const result = memory.slice(ptr, ptr + len);
// Process large files in chunks to stay within memory limits
async function processLargeFile(
file: File,
chunkSize: number = 1024 * 1024 // 1MB chunks
): Promise<Uint8Array[]> {
const results: Uint8Array[] = [];
const reader = file.stream().getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const processed = await processChunkWithWasm(value);
results.push(processed);
}
return results;
}