Rust compiles to WebAssembly with near-native performance, enabling compute-intensive code — image processing, cryptography, parsing, numerical algorithms — to run in the browser without a server round-trip. wasm-bindgen generates the JavaScript glue code for calling Rust functions and passing complex types across the boundary. wasm-pack wraps the whole workflow: compile, generate bindings, and publish to npm. Claude Code writes the Rust WASM modules, JS integration code, Web Worker wrappers for non-blocking execution, and the build configuration.
CLAUDE.md for Rust WASM Projects
## WASM Stack
- Rust edition 2021 with wasm-pack 0.12+
- wasm-bindgen for JS interop (types, closures, DOM access)
- web-sys and js-sys for browser API access
- Worker: wasm in a Web Worker to avoid blocking the main thread
- Optimization: opt-level=z in release, wasm-opt via wasm-pack
- npm package output: pkg/ directory (wasm-pack build --target web)
- Testing: wasm-pack test --headless --chrome
- Toolchain: wasm32-unknown-unknown via rustup
Rust WASM Module with wasm-bindgen
// src/lib.rs
use wasm_bindgen::prelude::*;
// Export Rust functions to JavaScript
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Struct with methods — becomes a JS class
#[wasm_bindgen]
pub struct ImageProcessor {
width: u32,
height: u32,
pixels: Vec<u8>,
}
#[wasm_bindgen]
impl ImageProcessor {
// Constructor: called as new ImageProcessor(width, height)
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
Self { width, height, pixels: data }
}
// Method: processor.grayscale()
pub fn grayscale(&mut self) {
for chunk in self.pixels.chunks_mut(4) {
if chunk.len() < 3 { continue; }
let gray = (0.299 * chunk[0] as f32
+ 0.587 * chunk[1] as f32
+ 0.114 * chunk[2] as f32) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
// chunk[3] = alpha, unchanged
}
}
pub fn invert(&mut self) {
for chunk in self.pixels.chunks_mut(4) {
if chunk.len() < 3 { continue; }
chunk[0] = 255 - chunk[0];
chunk[1] = 255 - chunk[1];
chunk[2] = 255 - chunk[2];
}
}
pub fn brightness(&mut self, delta: i16) {
for chunk in self.pixels.chunks_mut(4) {
if chunk.len() < 3 { continue; }
for i in 0..3 {
chunk[i] = ((chunk[i] as i16 + delta).clamp(0, 255)) as u8;
}
}
}
// Return the processed pixel data
pub fn get_pixels(&self) -> Vec<u8> {
self.pixels.clone()
}
// Property getter: processor.pixel_count
#[wasm_bindgen(getter)]
pub fn pixel_count(&self) -> u32 {
self.width * self.height
}
}
// Passing JS objects with serde
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsValue;
#[derive(Serialize, Deserialize)]
pub struct FilterOptions {
grayscale: bool,
brightness: i16,
invert: bool,
}
#[wasm_bindgen]
pub fn apply_filters(data: Vec<u8>, width: u32, height: u32, options_js: JsValue) -> Vec<u8> {
let options: FilterOptions = serde_wasm_bindgen::from_value(options_js)
.unwrap_or(FilterOptions { grayscale: false, brightness: 0, invert: false });
let mut processor = ImageProcessor::new(width, height, data);
if options.grayscale { processor.grayscale(); }
if options.brightness != 0 { processor.brightness(options.brightness); }
if options.invert { processor.invert(); }
processor.get_pixels()
}
Cargo.toml Configuration
[package]
name = "image-processor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console", "Window", "Document"] }
[profile.release]
opt-level = "z" # Optimize for binary size
lto = true
codegen-units = 1
[dev-dependencies]
wasm-bindgen-test = "0.3"
JavaScript Integration
// src/imageWorker.js — WASM in a Web Worker (non-blocking)
import init, { ImageProcessor, apply_filters } from '../pkg/image_processor.js';
let initialized = false;
async function ensureInit() {
if (!initialized) {
await init();
initialized = true;
}
}
self.onmessage = async ({ data }) => {
await ensureInit();
const { type, id, payload } = data;
try {
let result;
if (type === 'applyFilters') {
const { imageData, options } = payload;
const processed = apply_filters(
imageData.data,
imageData.width,
imageData.height,
options,
);
// Return processed pixels back to main thread
// Use transferable for zero-copy
const buffer = processed.buffer;
self.postMessage({ id, type: 'result', buffer }, [buffer]);
return;
}
} catch (error) {
self.postMessage({ id, type: 'error', message: error.message });
}
};
// src/lib/wasmProcessor.ts — main thread wrapper
interface PendingRequest {
resolve: (data: ArrayBuffer) => void;
reject: (error: Error) => void;
}
let worker: Worker | null = null;
const pending = new Map<string, PendingRequest>();
let requestId = 0;
function getWorker(): Worker {
if (!worker) {
worker = new Worker(new URL('./imageWorker.js', import.meta.url), { type: 'module' });
worker.onmessage = ({ data }) => {
const req = pending.get(data.id);
if (!req) return;
pending.delete(data.id);
if (data.type === 'error') {
req.reject(new Error(data.message));
} else {
req.resolve(data.buffer);
}
};
}
return worker;
}
export async function processImage(
imageData: ImageData,
options: { grayscale?: boolean; brightness?: number; invert?: boolean },
): Promise<ImageData> {
const id = String(requestId++);
return new Promise((resolve, reject) => {
pending.set(id, {
resolve: (buffer) => {
const pixels = new Uint8ClampedArray(buffer);
resolve(new ImageData(pixels, imageData.width, imageData.height));
},
reject,
});
getWorker().postMessage({ type: 'applyFilters', id, payload: { imageData, options } });
});
}
Canvas Integration
// src/components/ImageEditor.tsx
import { useRef, useState, useCallback } from 'react';
import { processImage } from '../lib/wasmProcessor';
export function ImageEditor() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [processing, setProcessing] = useState(false);
const applyFilter = useCallback(async (
options: { grayscale?: boolean; brightness?: number; invert?: boolean }
) => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
setProcessing(true);
try {
// Runs in Web Worker via WASM — doesn't block main thread
const processed = await processImage(imageData, options);
ctx.putImageData(processed, 0, 0);
} finally {
setProcessing(false);
}
}, []);
const handleFileLoad = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const canvas = canvasRef.current!;
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d')!.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
};
img.src = url;
};
return (
<div>
<input type="file" accept="image/*" onChange={handleFileLoad} />
<canvas ref={canvasRef} style={{ maxWidth: '100%' }} />
<div className="controls">
<button onClick={() => applyFilter({ grayscale: true })} disabled={processing}>
Grayscale
</button>
<button onClick={() => applyFilter({ brightness: 30 })} disabled={processing}>
Brighter
</button>
<button onClick={() => applyFilter({ invert: true })} disabled={processing}>
Invert
</button>
</div>
{processing && <p>Processing with WASM...</p>}
</div>
);
}
Build and Publish
# Build for browser (ES module output)
wasm-pack build --target web --out-dir pkg
# Build for bundlers (webpack/vite)
wasm-pack build --target bundler --out-dir pkg
# Run tests in headless Chrome
wasm-pack test --headless --chrome
# Publish to npm
wasm-pack publish
For the Rust async runtime patterns used in WASM-adjacent Rust services, see the Rust async guide for tokio and async/await. For the broader WebAssembly component model beyond browser use cases, the WebAssembly guide covers WASI and the component model. The Claude Skills 360 bundle includes Rust WASM skill sets covering wasm-bindgen, Web Worker integration, and canvas processing patterns. Start with the free tier to try Rust WASM module generation.