Agent Skills: WASM Architecture Decision

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

UncategorizedID: arustydev/ai/wasm-architecture-decision

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/plugins/web/browser-extension-dev/skills/wasm-architecture-decision

Skill Files

Browse the full folder contents for wasm-architecture-decision.

Download Skill

Loading file tree…

content/plugins/web/browser-extension-dev/skills/wasm-architecture-decision/SKILL.md

Skill Metadata

Name
wasm-architecture-decision
Description
Decision framework for WebAssembly usage in browser extensions including loading patterns, architecture, and performance considerations

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

Decision Matrix

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:

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

Anti-Patterns

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

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

| 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!) |

When JavaScript is Faster

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

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

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

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