There are no OS threads in the browser β so how does Rust's parallel processing library Rayon actually work?
Here's a summary.
π€ Why Do We Need This?
When you run heavy computation (taking seconds to tens of seconds) on the browser's main thread, the UI completely freezes. Buttons become unclickable, scrolling stops, and you get the dreaded "This page is not responding" warning.
The solution is multithreading β offload heavy computation to the background, and split the work across multiple threads.
ποΈ Two-Layer Web Worker Architecture
Parallel computation in the browser requires Web Workers serving two distinct roles.
βββββββββββββββββββββββββββββββββββββββ
β π₯οΈ Main Thread (UI Thread) β
β β
β User interactions, screen rendering β
β β Always responsive β
ββββββββββββ¬ββββββββββββββββββββββββββββ
β π¨ postMessage
β
βββββββββββββββββββββββββββββββββββββββ
β π· Layer 1: Worker Thread (1) β
β β
β Takes on heavy computation β
β β Zero impact on the main thread β
β β
β βββββββββββ¬ββββββββββ¬ββββββββββ β
β β π #1 β π #2 β π #3 β β
β β rayon β rayon β rayon β β β Layer 2
β β Worker β Worker β Worker β β N Web Workers
β β β β β β
β β Part of β Part of β Part of β β
β β the workβ the workβ the workβ β
β βββββββββββ΄ββββββββββ΄ββββββββββ β
βββββββββββββββββββββββββββββββββββββββ
π‘οΈ Layer 1 β Protecting the UI
The role of the Layer 1 Web Worker is to move heavy computation out of the main thread.
Rayon doesn't come into play here yet β the purpose of this worker thread is simply to decouple from the main thread and keep it free.
An important note: even if you call a Rust async function from WASM, the internal CPU work synchronously blocks the thread. Being async doesn't prevent UI freezing. It's the Web Worker that sets the UI thread free.
β‘ Layer 2 β Splitting the Work
With Layer 1 alone, computation still runs sequentially on a single thread. Layer 2 splits the computation itself across multiple threads for actual speedup.
π¦ Rayon β Parallelise with .par_iter()
Rayon is Rust's parallel computation library. Usage is simple:
// π΄ Normal (single-threaded, sequential)
let results: Vec<_> = data.iter()
.map(|x| heavy_computation(x))
.collect();
// β‘ Rayon (multi-threaded, parallel)
let results: Vec<_> = data.par_iter() // β just add par_
.map(|x| heavy_computation(x))
.collect();
Change .iter() to .par_iter() β that's it. Splitting, dispatching, and collecting results is all handled automatically by Rayon.
π΄ 1 thread:
Thread 1: [Task 1, Task 2, ..., Task 524,288]
β One thread does everything sequentially
β‘ 8 threads (Rayon):
Thread 1: [Task 1 β 65,536] π
Thread 2: [Task 65,537 β 131,072] π
...
Thread 8: [Task 458,753 β 524,288] π
β 8 threads working simultaneously β ~8x faster
π Same Rust Code, Different Backend
The beauty of Rayon is that the same Rust code runs on both native and browser. Only the underlying thread implementation differs.
Same Rust code
data.par_iter()
β
ββββββββββ΄βββββββββ
β β
π₯οΈ Native π Browser
β β
OS threads wasm-bindgen-rayon
(pthread) β
β Web Worker Γ N
β β
Parallel β
Parallel β
- Native: Rayon β OS threads (pthread). No configuration needed β it just works.
-
Browser: OS threads don't exist β
wasm-bindgen-rayontranslates Rayon's thread requests into Web Workers.initThreadPool(N)spawns N Web Workers.
initThreadPool() is only needed in the browser. On native, Rayon automatically uses OS threads.
π Requirements for Browser Multithreading
Browser multithreading requires several conditions to be met for security reasons.
| Requirement | Why? |
|---|---|
| π HTTPS |
SharedArrayBuffer (shared memory between threads) is only available over HTTPS |
| π¦ COEP/COOP headers | Cross-origin isolation required as a Spectre vulnerability mitigation |
| βοΈ atomics flag | Enables inter-thread memory synchronisation at WASM compile time |
| π build-std | Rebuilds Rust's standard library from source with atomics support |
βοΈ Why is build-std needed?
The pre-built version of Rust's standard library (std) does not include atomics (shared memory operations). To share memory between threads, std must be rebuilt from source.
# β Pre-built std β no atomics β no multithreading
wasm-pack build --release --target web
# β
build-std rebuilds std β atomics enabled β multithreading works
CARGO_UNSTABLE_BUILD_STD=std,panic_abort wasm-pack build --release --target web
π‘ We pass this as an environment variable because Cargo's
[unstable]section is global and cannot be scoped to WASM targets only. Putting it in the config file would break native builds.
π― Summary
User triggers an action π±οΈ
β
β postMessage
β
π· Layer 1: Worker Thread (1)
β β Does not block the UI
β
ββ Rayon par_iter()
β β
β ββ π Worker #1: part of the work
β ββ π Worker #2: part of the work
β ...
β ββ π Worker #N: part of the work
β
β β All workers complete β results collected
β
β postMessage({ status: 'done' })
β
π₯οΈ Main Thread: display result β
β UI stayed responsive throughout π
π Three Key Takeaways
- Layer 1 = Protects the UI. Just one worker. Moves heavy computation off the main thread.
-
Layer 2 = Makes computation faster. N workers.
par_iter()automatically splits the work. - The same Rust code runs on both native and browser. Only the underlying thread implementation differs.
How was that?
Speed up your browser computation with Rust's parallel processing library Rayon β‘β‘β‘