Implementing WebAssembly (WASM) Modules for a Web Application
WebAssembly — a binary instruction format for a virtual machine built into the browser. Not a replacement for JavaScript — a supplement for tasks where native speed is critical: codecs, cryptography, image processing, physics engines, ML inference, CAD tools, emulators.
WASM runs in an isolated sandbox, shares memory with JS via WebAssembly.Memory (linear buffer ArrayBuffer), is called from JS like a regular function.
Three Ways to Get a WASM Module
1. Compilation from Rust (preferred)
Rust + wasm-pack — the best development experience today. Good typing, automatic bindings via wasm-bindgen, support for complex types.
cargo new --lib image-processor
cd image-processor
cargo add wasm-bindgen image
// src/lib.rs
use wasm_bindgen::prelude::*;
use image::{DynamicImage, ImageFormat};
use std::io::Cursor;
#[wasm_bindgen]
pub fn resize_image(data: &[u8], width: u32, height: u32) -> Vec<u8> {
let img = image::load_from_memory(data).unwrap();
let resized = img.resize_exact(width, height, image::imageops::FilterType::Lanczos3);
let mut output = Cursor::new(Vec::new());
resized.write_to(&mut output, ImageFormat::WebP).unwrap();
output.into_inner()
}
#[wasm_bindgen]
pub fn grayscale(data: &[u8]) -> Vec<u8> {
let img = image::load_from_memory(data).unwrap();
let gray = img.grayscale();
let mut output = Cursor::new(Vec::new());
gray.write_to(&mut output, ImageFormat::Png).unwrap();
output.into_inner()
}
wasm-pack build --target web --release
2. Compilation from C/C++ via Emscripten
For porting existing C libraries:
emcc processing.c \
-O3 \
-o processing.js \
-s WASM=1 \
-s EXPORTED_FUNCTIONS='["_process_data"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-s ALLOW_MEMORY_GROWTH=1
3. Ready-made WASM Packages
Many libraries already ship WASM: @ffmpeg/ffmpeg, @sqlite.org/sqlite-wasm, pdfium-wasm, @mlc-ai/web-llm.
Loading and Initialization
// wasm-loader.ts
interface WasmModule {
resize_image: (data: Uint8Array, width: number, height: number) => Uint8Array
grayscale: (data: Uint8Array) => Uint8Array
memory: WebAssembly.Memory
}
let moduleInstance: WasmModule | null = null
async function loadWasmModule(): Promise<WasmModule> {
if (moduleInstance) return moduleInstance
// Streaming compilation — faster than ArrayBuffer
const response = await fetch('/wasm/image-processor.wasm')
const { instance } = await WebAssembly.instantiateStreaming(response, {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 4096 }),
},
})
moduleInstance = instance.exports as unknown as WasmModule
return moduleInstance
}
// Or via wasm-pack generated bindings
async function loadRustWasm() {
const { default: init, resize_image, grayscale } = await import('./pkg/image_processor')
await init() // loads and initializes WASM
return { resize_image, grayscale }
}
Data Transfer: JS ↔ WASM
WASM works only with numbers (i32, i64, f32, f64). Strings and arrays require memory management:
// Manual memory management (without wasm-bindgen)
async function callWasmWithData(
wasmModule: WebAssembly.Instance,
inputData: Uint8Array
): Promise<Uint8Array> {
const exports = wasmModule.exports as {
alloc: (size: number) => number
dealloc: (ptr: number, size: number) => void
process: (ptr: number, len: number) => number
memory: WebAssembly.Memory
}
const memory = new Uint8Array(exports.memory.buffer)
// Allocate memory in WASM and copy data
const inputPtr = exports.alloc(inputData.length)
memory.set(inputData, inputPtr)
// Call function — returns pointer to result
const resultPtr = exports.process(inputPtr, inputData.length)
// Read result length (first 4 bytes — convention)
const resultLen = new DataView(exports.memory.buffer).getUint32(resultPtr, true)
const result = new Uint8Array(exports.memory.buffer, resultPtr + 4, resultLen).slice()
// Free memory
exports.dealloc(inputPtr, inputData.length)
exports.dealloc(resultPtr, resultLen + 4)
return result
}
With wasm-pack/wasm-bindgen this boilerplate is generated automatically.
Running WASM in Web Worker
Heavy WASM operations should be moved to a Worker, otherwise main thread blocks:
// wasm-worker.ts
import init, { resize_image } from './pkg/image_processor'
let initialized = false
self.onmessage = async (event: MessageEvent) => {
const { id, type, payload } = event.data
if (!initialized) {
await init()
initialized = true
}
try {
switch (type) {
case 'RESIZE': {
const { imageData, width, height } = payload
const result = resize_image(new Uint8Array(imageData), width, height)
// Transferable — without copying
self.postMessage(
{ id, type: 'RESULT', payload: result.buffer },
[result.buffer]
)
break
}
}
} catch (error) {
self.postMessage({ id, type: 'ERROR', payload: (error as Error).message })
}
}
// main.ts — usage
const worker = new Worker(new URL('./wasm-worker.ts', import.meta.url), {
type: 'module',
})
async function resizeImage(file: File, width: number, height: number): Promise<Blob> {
const buffer = await file.arrayBuffer()
return new Promise((resolve, reject) => {
const id = Math.random().toString(36).slice(2)
const handler = (event: MessageEvent) => {
if (event.data.id !== id) return
worker.removeEventListener('message', handler)
if (event.data.type === 'ERROR') {
reject(new Error(event.data.payload))
} else {
resolve(new Blob([event.data.payload], { type: 'image/webp' }))
}
}
worker.addEventListener('message', handler)
// Transferable — transfer buffer without copying
worker.postMessage({ id, type: 'RESIZE', payload: { imageData: buffer, width, height } }, [buffer])
})
}
Example: SQLite in Browser
import { createDbWorker } from 'sql.js-httpvfs'
async function initDatabase() {
const worker = await createDbWorker(
[{ from: 'inline', config: { serverMode: 'full', url: '/db/products.sqlite3', requestChunkSize: 4096 } }],
'/sqlite.worker.js',
'/sql-wasm.wasm'
)
const results = await worker.db.query(
'SELECT id, name, price FROM products WHERE category = ? ORDER BY price LIMIT 20',
['electronics']
)
return results
}
Loading Optimizations
<!-- Preload WASM -->
<link rel="preload" href="/wasm/processor.wasm" as="fetch" type="application/wasm" crossorigin>
// Cache compiled module via Cache API
async function loadWithCache(url: string): Promise<WebAssembly.Module> {
const cache = await caches.open('wasm-modules-v1')
const cached = await cache.match(url)
if (cached) {
const buffer = await cached.arrayBuffer()
return WebAssembly.compile(buffer)
}
const response = await fetch(url)
await cache.put(url, response.clone())
return WebAssembly.compileStreaming(response)
}
What's Included
Choice of compilation language (Rust/C/ready package) for the task, wasm-pack or Emscripten build setup, implementing JS bindings, integration with Web Worker for non-blocking execution, HTTP headers setup (Content-Type: application/wasm, COEP/COOP for SharedArrayBuffer), binary size optimization.
WASM binaries require additional HTTP headers for SharedArrayBuffer:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Timeline: 3–5 days depending on algorithm complexity and need to write Rust/C code from scratch.







