🧡 Run Multi Threads With Rust On Browser β€” Rayon And 2 Layers Web Worker Mechanism

rust dev.to

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β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

πŸ›‘οΈ 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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🌍 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 βœ…
Enter fullscreen mode Exit fullscreen mode
  • Native: Rayon β†’ OS threads (pthread). No configuration needed β€” it just works.
  • Browser: OS threads don't exist β†’ wasm-bindgen-rayon translates 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
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ 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 πŸŽ‰
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ Three Key Takeaways

  1. Layer 1 = Protects the UI. Just one worker. Moves heavy computation off the main thread.
  2. Layer 2 = Makes computation faster. N workers. par_iter() automatically splits the work.
  3. 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 ⚑⚑⚑

Source: dev.to

arrow_back Back to Tutorials