Rust + WASM for Browser Graphics
The Rust + WASM + WebGPU stack is the current state of the art for building Figma-class applications.
The Modern Stack
Rust (application logic + rendering)
↓ wasm-pack / wasm-bindgen
WebAssembly (near-native performance)
↓ wgpu
WebGPU (GPU compute + graphics)
Why Rust
- Memory safety without GC: No pause jitter
- Zero-cost abstractions: Performance of C++
- Excellent WASM tooling: wasm-bindgen, wasm-pack
- Strong ecosystem: wgpu, vello, lyon for graphics
Performance Reality (2025)
Major browsers now achieve 95%+ of native performance for CPU-intensive WASM tasks.
V8 (Chrome) has integrated:
- 16-bit float support for WebGPU
- Packed integer dot products
- Memory64 for larger AI models
Key Libraries
wgpu
Cross-platform WebGPU implementation in Rust. Runs on:
- Native (Vulkan, Metal, DX12)
- Browser (WebGPU, WebGL2 fallback)
Same code targets both native and web.
Vello
GPU compute-centric 2D renderer. The Skia/Cairo replacement.
- 177 fps on paris-30k test scene (M1 Max, 1600px)
- Uses prefix-sum algorithms for GPU parallelization
- Eliminates CPU sorting/clipping bottlenecks
// Vello API (PostScript-inspired)
scene.fill(Fill::NonZero, transform, &brush, None, &path);
Lyon
Path tessellation library. Turns vector paths into GPU-friendly triangles.
Use when you need lower-level control than Vello.
Data Passing Patterns
Critical: Calling Rust from JS is cheap. Moving data isn’t.
The Cost Hierarchy
| Pattern | Cost | Use Case |
|---|---|---|
| Primitive (u32, f64) | ~1ns | Coordinates, indices |
| Small struct (via bindgen) | ~10ns | Config objects |
| Typed array view | ~100ns | Buffer handoff |
| Vec copy | ~1μs/KB | Last resort |
| Serialization (JSON/bincode) | ~10μs/KB | Avoid if possible |
Patterns
// Bad: Clone data
#[wasm_bindgen]
pub fn process(data: Vec<u8>) -> Vec<u8>
// Better: Pass typed views over buffers
#[wasm_bindgen]
pub fn process(data: &[u8], out: &mut [u8])
// Best: Zero-copy with SharedArrayBuffer
#[wasm_bindgen]
pub struct Renderer {
buffer: Vec<u8>, // Owned by WASM
}
#[wasm_bindgen]
impl Renderer {
pub fn buffer_ptr(&self) -> *const u8 { self.buffer.as_ptr() }
pub fn buffer_len(&self) -> usize { self.buffer.len() }
}
// JS side:
const ptr = renderer.buffer_ptr();
const len = renderer.buffer_len();
const view = new Uint8Array(memory.buffer, ptr, len);
// view is now a direct window into WASM memory
Memory Layout Gotcha
WASM linear memory can grow, invalidating all TypedArray views. Always re-create views after any call that might allocate:
// Danger: view may be stale after this call
renderer.add_object(...); // might grow memory
view[0] = 255; // UNDEFINED BEHAVIOR if memory grew
// Safe: recreate view after potential allocation
renderer.add_object(...);
const freshView = new Uint8Array(memory.buffer, ptr, len);
freshView[0] = 255;
New WASM Features (2025)
WasmGC: Garbage collection landed in all major browsers (Chrome 119+, Firefox 120+, Safari 18.2+). Enables languages like Go/Kotlin to target WASM efficiently.
SIMD: Parallel data processing. 2-4x speedup for math-heavy code.
Memory64: Large address spaces for AI models.
Build Pipeline
# Install toolchain
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
# Build for web
wasm-pack build --target web
# Output: pkg/ with .wasm + JS bindings
Architecture Pattern
┌─────────────────────────────────────┐
│ JS/TS UI Layer │
│ (React, Svelte, etc.) │
└──────────────┬──────────────────────┘
│ wasm-bindgen
┌──────────────▼──────────────────────┐
│ Rust Core │
│ • Document model │
│ • Business logic │
│ • Rendering commands │
└──────────────┬──────────────────────┘
│ wgpu
┌──────────────▼──────────────────────┐
│ WebGPU │
│ • GPU compute │
│ • Rasterization │
└─────────────────────────────────────┘
This is essentially Figma’s architecture, but with Rust instead of C++.
Sources:
Related: building figma today, vello gpu vector graphics, webgpu vs webgl