Some extension features require more computational power than JavaScript alone can efficiently provide. WebAssembly (Wasm) enables near-native performance for tasks like image processing, cryptography, data compression, and complex algorithms—all within the browser security model.
When to Use WebAssembly
WebAssembly excels at:
- CPU-intensive calculations: Hashing, compression, encoding
- Image/video processing: Filters, resizing, format conversion
- Cryptography: Encryption, key derivation, signing
- Data parsing: Binary formats, large datasets
- Porting existing code: C/C++/Rust libraries to web
When NOT to use Wasm:
- DOM manipulation (Wasm can't access DOM directly)
- Simple operations (JS overhead for Wasm calls)
- Highly async workloads (Wasm is synchronous)
- Small code size is critical (Wasm adds bundle size)
Setting Up WebAssembly
Rust Setup (Recommended)
Rust has excellent WebAssembly support through wasm-pack:
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Create new Wasm project
cargo new --lib extension-wasm
cd extension-wasm
# Cargo.toml
[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]
lto = true
opt-level = "z" # Optimize for size
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn hash_data(data: &[u8]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
#[wasm_bindgen]
pub fn compress_string(input: &str) -> Vec<u8> {
// Simple RLE compression example
let bytes = input.as_bytes();
let mut result = Vec::new();
let mut i = 0;
while i < bytes.len() {
let byte = bytes[i];
let mut count = 1u8;
while i + (count as usize) < bytes.len()
&& bytes[i + (count as usize)] == byte
&& count < 255
{
count += 1;
}
result.push(count);
result.push(byte);
i += count as usize;
}
result
}
#[wasm_bindgen]
pub fn decompress_to_string(compressed: &[u8]) -> String {
let mut result = Vec::new();
let mut i = 0;
while i < compressed.len() {
let count = compressed[i] as usize;
let byte = compressed[i + 1];
for _ in 0..count {
result.push(byte);
}
i += 2;
}
String::from_utf8(result).unwrap_or_default()
}
Build for browser:
wasm-pack build --target web --release
C/C++ with Emscripten
For existing C/C++ code:
// image_process.c
#include <emscripten.h>
#include <stdint.h>
EMSCRIPTEN_KEEPALIVE
void grayscale(uint8_t* pixels, int width, int height) {
for (int i = 0; i < width * height; i++) {
int offset = i * 4; // RGBA
uint8_t r = pixels[offset];
uint8_t g = pixels[offset + 1];
uint8_t b = pixels[offset + 2];
// Luminance formula
uint8_t gray = (uint8_t)(0.299 * r + 0.587 * g + 0.114 * b);
pixels[offset] = gray;
pixels[offset + 1] = gray;
pixels[offset + 2] = gray;
// Alpha unchanged
}
}
EMSCRIPTEN_KEEPALIVE
void blur(uint8_t* pixels, int width, int height, int radius) {
// Box blur implementation
uint8_t* temp = malloc(width * height * 4);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rSum = 0, gSum = 0, bSum = 0, count = 0;
for (int dy = -radius; dy <= radius; dy++) {
for (int dx = -radius; dx <= radius; dx++) {
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
int offset = (ny * width + nx) * 4;
rSum += pixels[offset];
gSum += pixels[offset + 1];
bSum += pixels[offset + 2];
count++;
}
}
}
int offset = (y * width + x) * 4;
temp[offset] = rSum / count;
temp[offset + 1] = gSum / count;
temp[offset + 2] = bSum / count;
temp[offset + 3] = pixels[offset + 3]; // Keep alpha
}
}
memcpy(pixels, temp, width * height * 4);
free(temp);
}
Compile with Emscripten:
emcc image_process.c -O3 \
-s WASM=1 \
-s EXPORTED_FUNCTIONS='["_grayscale", "_blur", "_malloc", "_free"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
-o image_process.js
Loading WebAssembly in Extensions
From Rust/wasm-pack
// wasm-loader.js
let wasmModule = null;
async function loadWasm() {
if (wasmModule) return wasmModule;
// Load from extension bundle
const wasmUrl = chrome.runtime.getURL('wasm/extension_wasm_bg.wasm');
const response = await fetch(wasmUrl);
const bytes = await response.arrayBuffer();
// Import the generated JS bindings
const { default: init, hash_data, compress_string, decompress_to_string } =
await import(chrome.runtime.getURL('wasm/extension_wasm.js'));
// Initialize with the wasm bytes
await init(bytes);
wasmModule = { hash_data, compress_string, decompress_to_string };
return wasmModule;
}
// Usage
async function processData(data) {
const wasm = await loadWasm();
const hash = wasm.hash_data(new Uint8Array(data));
return hash;
}
From Emscripten
// emscripten-loader.js
let Module = null;
async function loadEmscriptenModule() {
if (Module) return Module;
// Load the generated JS file
const script = document.createElement('script');
script.src = chrome.runtime.getURL('wasm/image_process.js');
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
// Wait for Module to be ready
Module = await new Promise(resolve => {
window.Module = {
onRuntimeInitialized: function() {
resolve(window.Module);
},
locateFile: function(file) {
return chrome.runtime.getURL('wasm/' + file);
}
};
});
return Module;
}
// Wrapped functions
async function grayscale(imageData) {
const module = await loadEmscriptenModule();
const { width, height, data } = imageData;
// Allocate memory in Wasm heap
const ptr = module._malloc(data.length);
module.HEAPU8.set(data, ptr);
// Call Wasm function
module._grayscale(ptr, width, height);
// Read result back
const result = new Uint8ClampedArray(module.HEAPU8.subarray(ptr, ptr + data.length));
// Free memory
module._free(ptr);
return new ImageData(result, width, height);
}
Real-World Use Cases
Image Processing Pipeline
// image-processor.js
class WasmImageProcessor {
constructor() {
this.module = null;
this.ready = this.initialize();
}
async initialize() {
this.module = await loadWasmModule();
}
async processImage(canvas, operations) {
await this.ready;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Apply operations in sequence
for (const op of operations) {
switch (op.type) {
case 'grayscale':
await this.grayscale(imageData);
break;
case 'blur':
await this.blur(imageData, op.radius);
break;
case 'sharpen':
await this.sharpen(imageData, op.amount);
break;
case 'resize':
imageData = await this.resize(imageData, op.width, op.height);
break;
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
async grayscale(imageData) {
const ptr = this.allocateImageData(imageData);
this.module._grayscale(ptr, imageData.width, imageData.height);
this.copyBackImageData(imageData, ptr);
this.module._free(ptr);
}
async blur(imageData, radius) {
const ptr = this.allocateImageData(imageData);
this.module._blur(ptr, imageData.width, imageData.height, radius);
this.copyBackImageData(imageData, ptr);
this.module._free(ptr);
}
allocateImageData(imageData) {
const ptr = this.module._malloc(imageData.data.length);
this.module.HEAPU8.set(imageData.data, ptr);
return ptr;
}
copyBackImageData(imageData, ptr) {
const result = this.module.HEAPU8.subarray(ptr, ptr + imageData.data.length);
imageData.data.set(result);
}
}
// Usage in content script
const processor = new WasmImageProcessor();
async function processPageImages() {
const images = document.querySelectorAll('img');
for (const img of images) {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
await processor.processImage(canvas, [
{ type: 'sharpen', amount: 0.5 }
]);
img.src = canvas.toDataURL();
}
}
Memory Management: WebAssembly uses linear memory. Always free allocated memory to prevent leaks. Consider using a memory pool for frequently allocated/freed buffers.
Cryptography Module
// src/crypto.rs
use wasm_bindgen::prelude::*;
use sha2::{Sha256, Digest};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, KeyInit};
#[wasm_bindgen]
pub fn sha256(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
#[wasm_bindgen]
pub fn encrypt_aes_gcm(key: &[u8], nonce: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, JsValue> {
if key.len() != 32 {
return Err(JsValue::from_str("Key must be 32 bytes"));
}
if nonce.len() != 12 {
return Err(JsValue::from_str("Nonce must be 12 bytes"));
}
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
let nonce = Nonce::from_slice(nonce);
cipher.encrypt(nonce, plaintext)
.map_err(|e| JsValue::from_str(&format!("Encryption failed: {}", e)))
}
#[wasm_bindgen]
pub fn decrypt_aes_gcm(key: &[u8], nonce: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, JsValue> {
if key.len() != 32 {
return Err(JsValue::from_str("Key must be 32 bytes"));
}
if nonce.len() != 12 {
return Err(JsValue::from_str("Nonce must be 12 bytes"));
}
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
let nonce = Nonce::from_slice(nonce);
cipher.decrypt(nonce, ciphertext)
.map_err(|e| JsValue::from_str(&format!("Decryption failed: {}", e)))
}
Data Compression for Storage
// Use Wasm-based compression to reduce storage usage
class WasmStorage {
constructor() {
this.wasm = null;
this.ready = this.init();
}
async init() {
this.wasm = await loadWasm();
}
async set(key, value) {
await this.ready;
const json = JSON.stringify(value);
const compressed = this.wasm.compress_string(json);
// Store compressed data
await chrome.storage.local.set({
[key]: {
compressed: true,
data: Array.from(compressed)
}
});
// Log compression ratio
const ratio = (compressed.length / json.length * 100).toFixed(1);
console.log(`Compressed ${key}: ${json.length} -> ${compressed.length} (${ratio}%)`);
}
async get(key) {
await this.ready;
const result = await chrome.storage.local.get(key);
const stored = result[key];
if (!stored) return null;
if (stored.compressed) {
const decompressed = this.wasm.decompress_to_string(new Uint8Array(stored.data));
return JSON.parse(decompressed);
}
return stored;
}
}
Performance Optimization
Memory Pooling
// Avoid repeated allocations
class WasmMemoryPool {
constructor(module, blockSize = 1024 * 1024) {
this.module = module;
this.blockSize = blockSize;
this.pools = new Map();
}
acquire(size) {
// Round up to next power of 2 for pooling
const poolSize = Math.pow(2, Math.ceil(Math.log2(size)));
if (!this.pools.has(poolSize)) {
this.pools.set(poolSize, []);
}
const pool = this.pools.get(poolSize);
if (pool.length > 0) {
return pool.pop();
}
return this.module._malloc(poolSize);
}
release(ptr, size) {
const poolSize = Math.pow(2, Math.ceil(Math.log2(size)));
const pool = this.pools.get(poolSize);
if (pool && pool.length < 10) { // Max 10 per size
pool.push(ptr);
} else {
this.module._free(ptr);
}
}
clear() {
for (const [size, pool] of this.pools) {
for (const ptr of pool) {
this.module._free(ptr);
}
}
this.pools.clear();
}
}
Worker Thread Integration
// wasm-worker.js
let wasm = null;
self.onmessage = async (e) => {
const { type, id, data } = e.data;
try {
if (!wasm) {
wasm = await loadWasm();
}
let result;
switch (type) {
case 'hash':
result = wasm.hash_data(new Uint8Array(data));
break;
case 'compress':
result = Array.from(wasm.compress_string(data));
break;
case 'process_image':
result = await processImageInWorker(data);
break;
}
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
};
// main.js - Worker management
class WasmWorkerPool {
constructor(workerCount = navigator.hardwareConcurrency || 4) {
this.workers = [];
this.queue = [];
this.pending = new Map();
this.nextId = 0;
for (let i = 0; i < workerCount; i++) {
const worker = new Worker(chrome.runtime.getURL('wasm-worker.js'));
worker.onmessage = this.handleMessage.bind(this);
this.workers.push({ worker, busy: false });
}
}
async execute(type, data) {
const id = this.nextId++;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.queue.push({ id, type, data });
this.processQueue();
});
}
processQueue() {
const available = this.workers.find(w => !w.busy);
if (!available || this.queue.length === 0) return;
const task = this.queue.shift();
available.busy = true;
available.currentId = task.id;
available.worker.postMessage(task);
}
handleMessage(e) {
const { id, result, error } = e.data;
const handler = this.pending.get(id);
if (handler) {
if (error) {
handler.reject(new Error(error));
} else {
handler.resolve(result);
}
this.pending.delete(id);
}
const worker = this.workers.find(w => w.currentId === id);
if (worker) {
worker.busy = false;
worker.currentId = null;
}
this.processQueue();
}
}
Summary
WebAssembly unlocks performance-critical functionality in browser extensions while maintaining security. Use it for CPU-bound tasks, leverage Rust or C++ ecosystems, and combine with Web Workers for parallel processing.
Key implementation points:
- Choose Rust for new Wasm code (best tooling)
- Use Emscripten to port existing C/C++ code
- Manage Wasm memory carefully (pools, explicit frees)
- Offload to Web Workers for heavy computation
- Profile before and after to validate performance gains
- Consider bundle size impact (tree-shake unused code)