Skip to content

Benchmark

The hard case is thousands of machines reacting to thousands of events inside one frame budget: trading terminals, live order books, monitoring walls, dense collaborative canvases. That’s what this engine is built for.

Watch it live: all three engines driving a real grid in your browser. Watch which ones fall behind as the load climbs.

The one decision behind every number here: Dunky mutates context in place. A transition writes the field and stops, it means no new snapshot, no immutable copy, nothing allocated. That’s what makes the hot path fast and the memory flat (the engine holds one context object per machine for its whole life, no matter how many events hit it). Immutable machines takes the opposite stance, a fresh snapshot per transition, which is why it allocates on every event.

These were run on a MacBook Pro (M1, 32 GB). Your absolute numbers will differ, so run it yourself and see.

Why a column is sometimes blank. The three engines don’t run the same way, so not every test fits all three. The cell is marked, the same way everywhere:

  • n/a ᵃ: async. Zag’s send is microtask-batched, so it can’t run in a synchronous ops/sec or flushSync loop, only where it runs synchronously (construction, memory, React rendering).
  • n/a ᶠ: no equivalent feature. The engine has no first-class primitive for that scenario (e.g. XState has no lazy/memoized computed), so there’s nothing comparable to time.

Reading the ops/sec tables: higher is better, and a gap between two rows is only real if it clears both rows’ run-to-run noise (±rme). A 1.0× / 1.2× difference is a tie; the 8×–33× gaps are the point.

DunkyXStateZag
Event throughput7.2 M ops/s897 Kn/a ᵃ
Memory, 2-field context3.6 KB3.6 KB9.1 KB
Memory, 64-field context4.1 KB4.1 KB134 KB
Re-render wall, 1000 rows3.9 ms6.8 msn/a ᵃ

ᵃ Zag’s send is microtask-batched; can’t run in a synchronous ops/sec loop.

A single machine, one event, tight loop:

DunkyXStateZag
ops/sec7.2 M898 Kn/a ᵃ

XState allocates a new immutable snapshot on every transition. Dunky mutates context in place, so a transition allocates nothing.

Change a field no observer has selected. The dedup layer re-evaluates and value-compares, and no listener fires:

ObserversDunky (ops/s)XState (ops/s)
1 0004.5 M536 K
5 0001.9 M453 K

XState’s actor.subscribe is coarse: it fires on every snapshot change. To match Dunky’s behavior you’d add a differ in the listener, which is what the xstate column already does, for a fair comparison.

One machine with N observers; bump one field and measure how many of these “wake only the one affected observer” cycles complete per second (higher is better):

ObserversDunky (ops/s)XState (ops/s)
100325 K253 K
1 00010.7 K10.7 K
5 0007.9 K741

Roughly par at small N. The gap widens with N because a coarse subscribe re-runs every listener on each change, while a fine-grained selection wakes only the affected one.

The whole point of the plain-object model: memory grows with your data, not with the framework’s bookkeeping.

Context widthDunkyXStateZag
2 fields3.6 KB3.6 KB9.1 KB
64 fields4.1 KB4.1 KB134 KB

Going 2 → 64 fields costs Dunky ~0.5 KB/machine. Zag allocates one reactive cell per field, so a 64-field context grows to 134 KB/machine.

Spin-up cost per machine:

DunkyXStateZag
µs / machine2.421.958.16

XState cold-starts ~1.2× faster; Zag is ~3.4× slower than both. A one-time cost paid at start(); see the trade-off.

These loads are extreme by design: they only reflect real software when a single view holds thousands of independently-stateful, live-updating cells in one frame budget:

WorkloadLive cells
Full L2 order book (Bookmap, Sierra Chart)3k – 15k
Options chain / vol surface (thinkorswim, Tastytrade)5k – 20k
Screeners / heatmaps (TradingView, Finviz)3k – 15k
Live-data spreadsheets (Excel + market feeds)5k – 20k
Observability walls (Grafana, Datadog)5k – 50k
Dense collaborative canvases (Figma, tldraw)5k – 50k

The sharpest target is a dense live financial surface: 5k–20k live cells, updating tens of thousands of times per second, often needed on web and native from one codebase, where every property this benchmark measures pays at once.

Full methodology, fairness notes, and all per-scenario tables in the benchmark README.