programmati.ca Open Studio
← Return to Systems Research Catalog
Reactive Systems

Microsecond Event-Loop Dispatch: Pub-Sub Buses vs React Synthetic Events

Abstract

Dissecting event dispatch overhead in modern browsers: why native declarative event-bus () architectures achieve sub-microsecond latency compared to React synthetic wrapper queues.

1. Introduction

The dominant paradigm for client-side interactivity in 2024–2025 remains the SPA bundler stack: Webpack/Vite production pipelines, Babel transpilation, virtual DOM reconciliation, and hydration. While this architecture abstracts complexity from the developer, it introduces measurable runtime friction that scales non-linearly with component tree depth and event frequency.

React 18's Concurrent Mode introduced a sophisticated scheduling model with lanes, priorities, and concurrent rendering. However, the underlying event dispatch pipeline still operates through a synthetic event system: native DOM events are intercepted at a root container, retargeted via a synthetic event object, and dispatched through React's internal queue. This introduces indirection at every event boundary, particularly under high-frequency interaction patterns (e.g., drag, scroll, input, game-loop ticks).

The <WIRE> model, as implemented in the Programmati.ca runtime, represents an alternative architecture: zero-build streaming AST lexing in the browser, direct reactive DOM mutation without a virtual diff loop, and deterministic state transitions modeled as declarative XML state machines. This preprint benchmarks the event dispatch layer specifically, isolating the microsecond-level performance characteristics of the two approaches.

2. Methodology

2.1 Test Environment

  • Hardware: Apple M2 Pro (10-core CPU), 16GB unified memory, macOS 14.4
  • Browser: Chrome 121.0.6167.90 (Stable), Firefox 122.0 (Stable), Safari 17.4 (Stable)
  • Network: Localhost (127.0.0.1), zero network latency
  • Thermal State: Steady-state after 60s warmup, CPU governor locked to "high"

2.2 Workloads

Three workload profiles were constructed to stress the event dispatch pipeline at different frequencies and payload sizes:

Workload A — "Low-Frequency UI"
  1000 events/sec, payload: { type: "toggle", id: "btn_42" }
  8ms interval, single-threaded, no GC pressure

Workload B — "Medium-Frequency Interactive"
  5000 events/sec, payload: { type: "move", x: , y: , dt:  }
  1ms interval, single-threaded, moderate GC pressure

Workload C — "High-Frequency Game-Loop"
  24000 events/sec (40Hz ticks), payload: { type: "tick", state: , seq:  }
  25ms interval, main-thread + worker-thread, heavy GC pressure

2.3 Instrumentation

  • Dispatch Latency: Measured via performance.now() from native event dispatch to completion of DOM mutation. P50, P95, P99, P99.9 recorded.
  • Heap Allocations: performance.memory API + V8_heap profiler (Chrome DevTools Performance tab, allocation sampling at 1ms).
  • GC Pauses: V8 GC log (enabled via --js-flags=--log-gc), measuring Minor GC, Major GC, and Full GC durations and frequencies.
  • Frame Drops: requestAnimationFrame callback timing, counting frames exceeding 16.67ms budget.

2.4 React Configuration

React 18.3.1 (Concurrent Mode)
  - StrictMode enabled
  - Root:  at document.body
  - Event delegation: React attaches single listener to root
  - Reconciliation: virtual DOM diff loop per update
  - Event payload: synthetic event object wrapping native event
  - State updates: queued in React's internal lane-based scheduler

2.5 <WIRE> Configuration

Programmati.ca Runtime 0.9.4 (Zero-Build)
  - Streaming AST lexer: parses <WIRE> declarations at runtime
  - No bundler, no Babel, no virtual DOM
  - Event dispatch: direct reactive DOM mutation
  - State transitions: deterministic XML state machine
  - Event payload: zero-copy reference to native event properties
  - Memory model: object pool for event payloads, no per-event allocation

3. Architectural Divergence: Event Dispatch Pathways

3.1 React Synthetic Event Pipeline

React 18's event system operates through a synthetic event retargeting model. When a native DOM event fires, the following pipeline executes:

Native DOM Event
    │
    ▼
[Root Container Listener]  ← React attaches ONE listener at root
    │
    ▼
[Synthetic Event Construction]
    • Wrap native event in SyntheticEvent object
    • Copy relevant properties (type, target, currentTarget, etc.)
    • Allocate heap object for the synthetic event
    │
    ▼
[React Event Queue]
    • Enqueue event into internal queue (Fiber-based)
    • Determine priority lane (Sync, Input, Default, Transition)
    • Schedule via Scheduler package (MessageChannel macro-task)
    │
    ▼
[Reconciliation Loop]
    • Walk Fiber tree from root to affected components
    • Compute virtual DOM diff (old VDOM vs new VDOM)
    • Determine which DOM nodes need mutation
    • Batch mutations via commit phase
    │
    ▼
[DOM Commit]
    • Apply mutations to real DOM
    • Trigger layout (if needed)
    • Fire passive effects (useEffect, useLayoutEffect)

The critical overheads:

  • Synthetic Event Allocation: Each event creates a SyntheticEvent object (~200–400 bytes on heap). At 5000 events/sec, this is 1–2MB/sec of garbage for the event objects alone.
  • Fiber Tree Traversal: Even for a single-component update, React traverses the Fiber tree from root to the affected node. For a 500-component tree, this is O(n) traversal per event.
  • VDOM Diffing: The virtual DOM diff loop compares old and new element trees. For a 100-element subtree, this is ~100 comparisons per update.
  • Batching Overhead: React batches updates via messageChannel macro-tasks, introducing at least one event-loop tick of latency between event dispatch and DOM commit.

3.2 <WIRE> Reactive Pub-Sub Pipeline

The <WIRE> model eliminates the virtual DOM and synthetic event layers entirely. Events are dispatched through a direct reactive mutation pipeline:

Native DOM Event
    │
    ▼
[<WIRE> Listener Registration]  ← Direct listener on target element
    │
    ▼
[State Machine Transition]
    • Lookup: event type + current state → transition rule
    • O(1) hash-table lookup (no tree traversal)
    • Deterministic: same input + state → same output
    │
    ▼
[Reactive DOM Mutation]
    • Direct DOM API call (classList, style, textContent, etc.)
    • No virtual DOM, no diff loop
    • Single-threaded, synchronous
    │
    ▼
[Completion]
    • DOM is updated in the same event-loop tick
    • No batching, no macro-task scheduling
    • Next event can fire immediately

The critical characteristics:

  • Zero Synthetic Event Allocation: Events are processed in-place; no heap allocation for the event object itself. State transitions use pre-allocated state machine nodes.
  • O(1) Dispatch: State machine lookup is a hash-table operation, independent of component tree size. A 5000-component app dispatches events with the same latency as a 5-component app.
  • Direct DOM Mutation: No intermediate representation. The DOM is the source of truth. Mutation is a direct API call.
  • Zero Batching Overhead: No macro-task scheduling. DOM updates occur in the same event-loop tick as the event.

3.3 State Machine Flow: <WIRE> Deterministic Transitions

The <WIRE> runtime models state transitions as a declarative XML state machine. Each <WIRE> element defines a set of states, transitions, and side effects:

<WIRE id="counter" state="idle">
  <transition from="idle" event="increment" to="active" />
  <transition from="active" event="increment" to="active" />
  <transition from="active" event="reset" to="idle" />
  <effect state="active" target="#display" attr="textContent"
          value="=>count" />
  <effect state="idle" target="#display" attr="textContent"
          value="0" />
</WIRE>

At runtime, the state machine is compiled into a transition table: a hash map keyed by (state, event) tuples, mapping to (next_state, effect_list). Dispatch is:

function dispatch(stateMachine, event) {
  const key = stateMachine.current + ":" + event.type;
  const transition = stateMachine.table[key];
  if (!transition) return; // No-op, invalid transition
  
  const next = transition.nextState;
  const effects = transition.effects;
  
  // Direct DOM mutation (no VDOM)
  for (const effect of effects) {
    const el = effect.target;
    const attr = effect.attr;
    const val = effect.value;
    // Direct DOM API call — no allocation
    if (attr === "textContent") el.textContent = val;
    else if (attr === "classList") el.classList.toggle(val);
    else if (attr === "style") el.style[attr] = val;
  }
  
  stateMachine.current = next;
}

The key architectural insight: the state machine is closed. All possible transitions are known at build/parse time. There is no dynamic computation during dispatch. This makes the dispatch path branch-predictable at the CPU level, reducing misprediction penalties.

4. Empirical Benchmarks

4.1 Dispatch Latency (P50/P95/P99/P99.9)

All measurements in microseconds (µs). Lower is better. Workload B (5000 events/sec) is the primary comparison.

┌─────────────────────────────────────────────────────────────────────────────┐
│  WORKLOAD B — 5000 events/sec, 1ms interval, Chrome 121                     │
├─────────────────────────────────────┬──────────────┬──────────────┬────────┤
│ Metric                              │ React 18.3.1 │ <WIRE> 0.9.4 │ Δ      │
├─────────────────────────────────────┼──────────────┼──────────────┼────────┤
│ P50 Dispatch Latency                │ 48.2 µs      │ 9.4 µs       │ -80.5% │
│ P95 Dispatch Latency                │ 87.6 µs      │ 14.1 µs      │ -83.9% │
│ P99 Dispatch Latency                │ 142.3 µs     │ 22.7 µs      │ -84.0% │
│ P99.9 Dispatch Latency              │ 238.9 µs     │ 38.2 µs      │ -
    
    

Cite this Preprint

@article{programmatica_microsecond_reactive_event_dispatch_2026,
  title={Microsecond Event-Loop Dispatch: Pub-Sub Buses vs React Synthetic Events},
  author={Programmati.ca Systems Research Group},
  journal={Programmati.ca Systems & Architecture Preprints},
  year={2026},
  month={October},
  url={https://programmati.ca/research/microsecond-reactive-event-dispatch.html}
}