Skip to content

RefCell against Mutex in WebAssembly: November 2026

Reference machine
AMD Ryzen 9 5900X 12-Core Processor (x86-64) · Ubuntu 26.04.1 LTS
Browsers
Chrome 153 · Firefox 159
Builds
panic=abort · panic=unwind · +atomics
Samples
20 rounds per cell
Browser
Measure on this machine

1. Main points

  • In the default build, a Mutex lock took 3× as long as a RefCell borrow in Chrome and 3.3× in Firefox (Figure 2).
  • With panic=unwind, a Mutex lock took 1.23× as long as with panic=abort in Chrome and 1.63× in Firefox (Figure 3).
  • With +atomics, a Mutex lock took 1.56× as long as without it in Chrome and 2.2× in Firefox (Figure 3).

2. Time per operation

Figure 1. Time per operation in the default build, Chrome 153

Nanoseconds per operation in the default build (panic=abort); for the held/ rows, per loop iteration. The dot is the median of 20 rounds; the line runs from the fastest round to the slowest. Lower is faster. Figure 3 and the results table cover the other two builds.

  • Median
  • Min–max

Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.

3. Mutex vs RefCell

Figure 2. In the default build, a Mutex lock took 3× as long as a RefCell borrow in Chrome

How many times longer the first operation takes than the second, in Chrome 153. Above 1× means the first is slower. Every round runs both, so each round gives one ratio; the dot is the median of 20 rounds. A ratio counts as a clear difference when it is more than 3% from 1× and its 95% confidence interval (the whisker) does not include 1×.

  • A clear difference
  • No clear difference
  • 95% CI
  • Min–max
  • ±3% threshold

Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.

4. panic=unwind and +atomics

Figure 3. With panic=unwind, a Mutex lock took 22.5% longer than with panic=abort in Chrome

How many times longer each operation takes in the panic=unwind and +atomics builds than in the default build, in Chrome 153. RefCell should not change between builds, so its row is the control. The WebAssembly is the same in every browser, so a difference between browsers comes from the browser's own compiler. A ratio counts as a clear difference when it is more than 3% from 1× and its 95% confidence interval (the whisker) does not include 1×.

  • A clear difference
  • No clear difference
  • 95% CI
  • Min–max
  • ±3% threshold

Arrow keys move between points; Enter pins one so its values stay shown; Escape clears.

5. Why the Mutex is slower

Figure 4. A RefCell borrow and a Mutex lock, as WebAssembly

Each operation is a Rust function marked #[inline(never)], shown as wasm-bindgen outputs it: exactly what the browser compiles. The time is the median from the results table for the chosen build. Instruction counts include the panic path, which never runs here.

Operations shown
Build shown

RefCell::borrow_mut

14 instructions · 0.720 ns in Chrome 153

*c.borrow_mut() += x

(func $k_refcell_write (param i32 i64)
  block ;; label = @1
    local.get 0
    i32.load
    br_if 0 (;@1;)
    local.get 0
    local.get 0
    i64.load offset=8
    local.get 1
    i64.add
    i64.store offset=8
    return
  end
  i32.const 1049652
  call $"core::cell::panic_already_borrowed"
  unreachable
)

Mutex::lock

42 instructions · 2.148 ns in Chrome 153

*m.lock().unwrap() += x

(func $k_mutex_lock (param i32 i64)
  (local i32 i32)
  global.get $__stack_pointer
  i32.const 16
  i32.sub
  local.tee 2
  global.set $__stack_pointer
  local.get 0
  i32.load8_u
  local.set 3
  local.get 0
  i32.const 1
  i32.store8
  local.get 2
  local.get 3
  i32.store8 offset=15
  block ;; label = @1
    local.get 3
    i32.const 1
    i32.ne
    br_if 0 (;@1;)
    i32.const 0
    local.get 2
    i32.const 15
    i32.add
    i32.const 1049579
    i32.const 1050308
    i32.const 65
    i32.const 1050340
    call $"core::panicking::assert_failed::<bool, bool>"
    unreachable
  end
  local.get 0
  i32.const 0
  i32.store8
  local.get 0
  local.get 0
  i64.load offset=8
  local.get 1
  i64.add
  i64.store offset=8
  local.get 2
  i32.const 16
  i32.add
  global.set $__stack_pointer
)

6. Results

Median nanoseconds per operation in Chrome 153. Under each time: how many times longer it took than its baseline row in the same build. Bold means the difference clears the threshold.
Workloadpanic=abortpanic=unwind+atomics
One call per operation
op/plain*c += x0.646 nsbaseline0.647 nsbaseline0.648 nsbaseline
op/cellc.set(c.get() + x)0.654 ns1.01× op/plain0.647 ns1.00× op/plain0.648 ns1.00× op/plain
op/atomic_relaxedc.fetch_add(x, Relaxed)0.650 ns1.00× op/plain0.649 ns1.00× op/plain1.948 ns2.98× op/plain
op/refcell_write*c.borrow_mut() += x0.720 ns1.11× op/plain0.724 ns1.11× op/plain0.720 ns1.11× op/plain
op/refcell_try_writeif let Ok(mut g) = c.try_borrow_mut() { *g += x }0.720 ns1.00× op/refcell_write0.721 ns1.00× op/refcell_write0.719 ns1.00× op/refcell_write
op/mutex*m.lock().unwrap() += x2.148 ns3.31× op/plain2.625 ns4.03× op/plain3.332 ns5.13× op/plain
op/mutex_tryif let Ok(mut g) = m.try_lock() { *g += x }1.288 ns0.60× op/mutex1.492 ns0.57× op/mutex3.406 ns1.02× op/mutex
op/rwlock_write*l.write().unwrap() += x0.728 ns1.11× op/plain1.967 ns3.04× op/plain3.898 ns6.00× op/plain
op/plain_read*c0.288 nsbaseline0.287 nsbaseline0.289 nsbaseline
op/refcell_read*c.borrow()0.469 ns1.63× op/plain_read0.468 ns1.63× op/plain_read0.467 ns1.63× op/plain_read
op/rwlock_read*l.read().unwrap()0.542 ns1.89× op/plain_read0.749 ns2.58× op/plain_read3.998 ns13.88× op/plain_read
Lock inside a loop
loop/plainv += x; *p = v0.433 nsbaseline0.435 nsbaseline0.437 nsbaseline
loop/cellp.set(p.get() + x)0.433 ns1.00× loop/plain0.432 ns1.00× loop/plain0.438 ns1.00× loop/plain
loop/atomic_relaxedp.fetch_add(x, Relaxed)0.510 ns1.18× loop/plain0.517 ns1.18× loop/plain1.955 ns4.46× loop/plain
loop/refcell_write*p.borrow_mut() += x1.108 ns2.55× loop/plain1.118 ns2.56× loop/plain1.114 ns2.56× loop/plain
loop/mutex*p.lock().unwrap() += x1.534 ns3.55× loop/plain1.806 ns4.14× loop/plain3.454 ns7.92× loop/plain
loop/rwlock_write*p.write().unwrap() += x1.185 ns2.73× loop/plain1.612 ns3.68× loop/plain3.916 ns8.95× loop/plain
Lock held for the whole loop
held/refcelllet mut g = p.borrow_mut(); for … { *g += x }0.433 ns1.00× loop/plain0.437 ns1.00× loop/plain0.435 ns1.00× loop/plain
held/mutexlet mut g = p.lock().unwrap(); for … { *g += x }0.434 ns1.00× loop/plain0.434 ns1.00× loop/plain0.436 ns1.00× loop/plain
Shared object with a method call
app/rc_refcells.borrow_mut().record(x)1.307 nsbaseline1.314 nsbaseline1.306 nsbaseline
app/arc_mutexs.lock().unwrap().record(x)1.623 ns1.24× app/rc_refcell1.907 ns1.46× app/rc_refcell3.390 ns2.59× app/rc_refcell
Measurement notes
Conditions
Clock
Method
Workloads
Builds
Design
Checks
Browsers inline
Firefox 159 capture
Build
Commit
Artifacts

7. Measure on this machine

Samples
Export after a run: CSV, JSON, Markdown