Why the Main Thread Blocks
JavaScript on the main thread is single-threaded. When a physics step takes 12 ms and the render loop needs 8 ms, you have 20 ms per frame — barely 50 FPS on a 60 Hz display. On a complex simulation with 2000 rigid bodies this is optimistic.
The fix is to separate concerns: the render thread calls
requestAnimationFrame and draws; the physics thread runs
Cannon-es and writes positions back. They communicate asynchronously.
Architecture: Two-Thread Pipeline
Every frame the main thread sends a step message to the
worker. The worker advances the simulation, packs body positions and
quaternions into a shared Float32Array, and transfers it
back. The render thread reads the array and updates the Three.js mesh
matrices.
Setting Up the Worker
Transferable ArrayBuffers (Zero-Copy)
By default, postMessage copies the data —
expensive for large buffers. Marking the underlying
ArrayBuffer as a transferable tells the browser
to move ownership to the other thread in constant time. The original
buffer becomes empty (detached) after the transfer:
Alternative: SharedArrayBuffer + Atomics
If you prefer polling over messaging (lower latency for high-frequency
updates), allocate a shared buffer both threads can read and write
simultaneously. Requires Cross-Origin-Isolation headers:
When NOT to Use Workers
The render loop must stay on the main thread.
requestAnimationFrame, canvas drawing, and Three.js
renderer.render() are not available in Workers.
Offloading physics is safe; offloading rendering requires
OffscreenCanvas (limited browser support and harder to
use with Three.js — not recommended at this time).
Other cases where workers are not helpful:
- Small body count (<200 bodies): postMessage overhead may exceed the computation savings.
- Tightly coupled physics + rendering: if your shader reads physics output every pixel (GPU particles), the transfer cost outweighs the benefit.
- Debugging: Workers make Chrome DevTools harder to use — prototype your physics on the main thread first.
Browser support: Web Workers are available in all
modern browsers. SharedArrayBuffer requires
Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp response
headers on your server — or the feature is disabled for security
reasons since Spectre.
Related Posts
Devlog #15 covers complementary performance techniques that work alongside Workers — adaptive quality presets, lazy Three.js loading, and battery-aware throttling.