When running heavy physics simulations or particle layouts in a Web Worker, JavaScript's execution speed can still become a bottleneck. Under severe loads (like calculating 100,000+ interactive gravity points), garbage collection pauses and floating-point math overhead cause dropped frames.
To push web rendering to its absolute limits, we can combine OffscreenCanvas with WebAssembly (Wasm). By compiling performance-critical logic from Rust or C++ into Wasm, we execute native-speed computations directly within our Web Worker.
The Wasm + Worker Pipeline
Instead of passing heavy datasets back and forth between JavaScript and WebAssembly (which incurs copying overhead), we write the entire simulation engine in Rust. The Rust engine manages the memory containing particle coordinates, velocities, and color arrays, updating them in place.
// Inside our Rust simulation engine (lib.rs)
#[wasm_bindgen]
pub struct ParticleSystem {
particles: Vec<Particle>,
width: f32,
height: f32,
}
#[wasm_bindgen]
impl ParticleSystem {
pub fn update(&mut self, mouse_x: f32, mouse_y: f32, active: bool) {
// High-performance vectorized loops in Rust
for p in self.particles.iter_mut() {
p.update_physics(mouse_x, mouse_y, active);
}
}
}Shared Memory Buffers
The main performance gain comes from direct memory mapping. The compiled WebAssembly module exposes its raw linear memory buffer to the JavaScript wrapper in the Web Worker.
We map a Float32 array directly over the Wasm memory space. JavaScript can read coordinates directly out of this buffer and paint them directly to the OffscreenCanvas context without any data serialization or cloning:
// Inside canvas-worker.ts
import init, { ParticleSystem } from './wasm/sim_engine.js';
let system: ParticleSystem;
let wasmMemory: WebAssembly.Memory;
self.onmessage = async (e) => {
if (e.data.type === 'INIT') {
const wasm = await init();
wasmMemory = wasm.memory;
system = ParticleSystem.new(e.data.width, e.data.height);
requestAnimationFrame(loop);
}
};
function loop() {
system.update(mouse.x, mouse.y, mouse.active);
// Directly point to the Wasm linear memory block
const particleData = new Float32Array(
wasmMemory.buffer,
system.get_particles_ptr(),
system.get_particles_count() * 4
);
drawParticles(particleData);
requestAnimationFrame(loop);
}Eliminating Garbage Collection Stutter
JavaScript's garbage collector runs periodically to free memory, causing micro-stutters (INP - Interaction to Next Paint issues). By allocating and managing all particle data in WebAssembly's static memory heap, we eliminate dynamic JS allocations. The render loop runs with zero garbage collection overhead.
Conclusion
By pairing WebAssembly with OffscreenCanvas, we effectively bypass the limitations of single-threaded JavaScript. The main thread handles user input and layout, the worker thread schedules frames, and WebAssembly executes vectorized physics math, yielding zero-latency interfaces at scale.
