Agents wasm-architecture-decision

Decision framework for WebAssembly usage in browser extensions including loading patterns, architecture, and performance considerations

install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/plugins/web/browser-extension-dev/skills/wasm-architecture-decision" ~/.claude/skills/arustydev-agents-wasm-architecture-decision && rm -rf "$T"
manifest: content/plugins/web/browser-extension-dev/skills/wasm-architecture-decision/SKILL.md
source content

WASM Architecture Decision

Decision framework for WebAssembly usage in browser extensions, including browser-specific loading patterns, architectural patterns, and performance considerations.

Decision Framework

When to Use WASM

Use CaseRecommendationRationale
Cryptographic operationsStrong yes10-100x faster, constant-time
Image/video processingStrong yesParallelizable, memory efficient
Compression/decompressionYesCPU-intensive, existing Rust libs
Complex parsing (binary)YesType safety, predictable perf
Scientific computingYesNumerical precision, speed
Simple data transformsNoOverhead exceeds benefit
DOM manipulationNoJS required, no benefit
API calls/networkingNoI/O bound, not CPU bound
String manipulationMaybeOnly for large-scale ops

Decision Matrix

Score each factor 1-5, then calculate totals:

FactorWeightScore
CPU intensity3x[1-5]
Data volume2x[1-5]
Performance criticality2x[1-5]
Existing Rust/C++ code2x[1-5]
Team Rust expertise1x[1-5]

Scoring guide:

  • Total 30+: Strong WASM candidate
  • Total 20-29: Consider WASM
  • Total <20: Use JavaScript

Anti-Patterns

PatternProblemAlternative
WASM for DOM accessImpossible, must call JSKeep DOM in JS layer
WASM for simple logicOverhead exceeds benefitNative JS
Frequent WASM↔JS callsCall overhead ~10μs eachBatch operations
Large data copiesMemory duplicationUse SharedArrayBuffer
Sync WASM in main threadBlocks UIWeb Worker or async

Browser-Specific Loading Patterns

Chrome (MV3 Service Worker)

// 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 (MV3 or MV2)

// 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

// 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 Script Loading

// 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();
}

Architecture Patterns

Pattern 1: Background-Only WASM

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;
  }
});

Pattern 2: Content Script WASM

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);
}

Pattern 3: Web Worker WASM

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
    );
  });
}

Pattern 4: Offscreen Document (Chrome MV3)

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);
}

Performance Benchmarks

Typical Performance Gains

OperationJS (ms)WASM (ms)Speedup
SHA-256 (1MB)4585.6x
AES-256 encrypt120158x
JSON parse (10MB)150403.75x
GZIP compress200355.7x
Image resize300506x
Regex (complex)80253.2x
Simple sum0.10.20.5x (slower!)

When JavaScript is Faster

ScenarioWhy JS Wins
<1ms operationsWASM call overhead dominates
Single string opsJS strings optimized
DOM manipulationMust call JS anyway
Small arrays (<1KB)Copy overhead dominates
JIT-optimized hot pathsV8/SpiderMonkey excellent

Benchmarking Template

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
  };
}

Memory Architecture

Extension Memory Limits

ContextChromeFirefoxSafari
Service Worker128MB512MB128MB
Event PageN/A512MBN/A
Content ScriptTab limitTab limitTab limit
Popup128MB128MB128MB

Memory-Efficient Patterns

// 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);

Streaming Processing

// 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;
}

Related Resources

  • wasm-extension-integration skill: Build pipeline and tooling
  • wasm-integration-advisor agent: Suitability analysis
  • wasm-decision-report style: Report format
  • /add-wasm-module command: Scaffolding tool