# tap — benchmark

Workload: 10_000 rows; passthrough side-effect callback. Tick updates
one row's `val`. Batch streams 1000 such ticks. Generated by
[comparisons/bench/operators/tap.bench.ts](../../comparisons/bench/operators/tap.bench.ts).

Two data variants measured: **bare** (`tap(() => {})`, 0-arg hot-path)
and **1-arg** (`tap(c => {})`, change-record form). CLAUDE.md notes the
bare path is 40%+ faster on batch because it skips per-row record
allocation.

| Library | Setup (ms) | Single (ms) | vs data | Batch 1000 (ms) | vs data |
|---|---:|---:|---:|---:|---:|
| **data** | 0.659 | **0.006** | — | **1.08** | — |
| svelte-store | 0.589 | 0.008 | 1.3× | 10.40 | 9.6× |
| rxjs | 0.590 | 0.009 | 1.5× | 10.11 | 9.4× |
| data (1-arg) | 9.68 | 0.012 | — | 1.66 | — |
| react | 0.784 | 0.056 | 9.3× | 34.24 | 31.7× |
| preact-signals | 1.29 | 0.251 | 41.8× | 23.55 | 21.8× |
| solid | 2.69 | 1.20 | 200× | 110.58 | 102× |
| mobx | 87.54 | 5.17 | 862× | 527.41 | 488× |
| vue-reactivity | 32.49 | 11.25 | 1875× | 1872.97 | 1734× |



crossfilter omitted — no analog primitive.

data (bare) is fastest on batch by ~6x over rxjs. On single, svelte-store
edges by 1µs (within measurement noise at this scale). Both data forms
beat every other peer comfortably.

## How

`tap` is a passthrough — it doesn't transform values, it relays each
verb to its sinks AND calls `fn` for the side effect. The bare 0-arg
path skips per-row change-record construction (the cloning + `{type,
key, value, at?}` object alloc). See operators/tap/index.ts and the
batching test in operators/tap/tap.perf.ts.

Run `BENCH_OPS=tap npm run bench:ops` to refresh.
