Skip to content

Benchmarks

Every number below is from this machine: Bun 1.3.14/JavaScriptCore, Darwin arm64, ambient load (no attempt made to get a quiet machine — these gates are designed to pass under normal development load, and re-run repeatedly to confirm it), measured 28-29 July 2026. Node was not measured this pass: this environment’s plain node can’t resolve tsx, so rather than pair a fresh Bun number against a stale Node one, Node is left out entirely.

Default to array’s eager operators. Reach for Iter when a chain stops early — find, take, head — on an input of 1,000 elements or more: eager loses there, and Iter wins 4-10x over lazy competitors on those shapes (5-884x over eager array). Compile whichever chain you’ve actually measured as hot with @stopcock/fp-compiler.

@stopcock/fp-compiler fuses a pipe/flow/compile chain into one loop at build time (see Fusion). The decision suite below runs 15 pipeline shapes at three sizes each (n=10 / 1,000 / 100,000) against ramda, rambda, remeda, ts-belt, and lodash. Values are operations per second, higher is better. stopcock pipe is uncompiled: plain sequential pipe, no build step. stopcock compiled is the identical chain run through @stopcock/fp-compiler. The last two columns are that row’s ratio against the fastest of the five competitor libraries; greater than 1x means stopcock was faster. - means the library doesn’t implement that operation (ts-belt has no scan; ts-belt has no takeWhile/flatMap composition used here).

Pipelinenstopcock pipestopcock compiledramdarambdaremedats-beltlodashpipe vs best libcompiled vs best lib
map109.2M39.6M12.4M6.6M2.3M7.3M3.6M0.74x3.19x
map1,000144.6k1.4M195.1k175.6k34.1k156.4k184.0k0.74x7.11x
map100,0001.1k6.4k1.7k1.4k2831.4k1.6k0.66x3.80x
map->filter104.8M22.2M5.2M3.1M1.5M4.5M2.0M0.93x4.31x
map->filter1,00065.4k249.4k78.9k56.7k20.7k74.1k71.6k0.83x3.16x
map->filter100,0004671.3k5504201515575840.80x2.15x
map->filter->reduce103.9M27.2M2.8M2.1M1.3M2.6M1.7M1.37x9.67x
map->filter->reduce1,00048.0k464.3k42.4k35.9k18.4k43.2k52.4k0.92x8.85x
map->filter->reduce100,0004014.6k3292711083254230.95x10.9x
map->filter->map->filter102.4M14.9M2.2M1.5M837.1k2.0M1.2M1.10x6.81x
map->filter->map->filter1,00030.2k133.1k30.1k22.9k10.9k27.7k32.1k0.94x4.15x
map->filter->map->filter100,000230911230185892282650.87x3.44x
8-op chain101.4M11.0M988.3k700.7k424.3k964.7k597.4k1.41x11.2x
8-op chain1,00014.4k113.7k13.6k10.8k6.1k13.5k16.6k0.87x6.86x
8-op chain100,00012187611385281091360.89x6.42x
map->filter->find (early exit near start)105.0M45.8M4.9M3.1M2.9M4.2M1.7M1.02x9.39x
map->filter->find (early exit near start)1,00066.7k44.6M79.8k57.0k1.1M76.0k72.7k0.06x41.4x
map->filter->find (early exit near start)100,0005283.0M65044817.2k5976040.03x174x
map->filter->find (early exit late)105.0M44.8M5.1M3.1M1.2M4.4M1.7M0.98x8.81x
map->filter->find (early exit late)1,00065.8k2.9M79.3k55.5k16.8k73.9k69.6k0.83x37.2x
map->filter->find (early exit late)100,00051530.9k6084291115435750.85x50.8x
map->filter->take(10)105.1M32.4M4.4M2.8M1.2M3.3M1.6M1.16x7.40x
map->filter->take(10)1,00067.7k27.2M79.9k57.0k960.9k56.9k2.0M0.03x13.6x
map->filter->take(10)100,00052527.4M599416926.2k4152.0M0.00x13.7x
takeWhile->map108.6M37.2M5.9M4.7M3.0M-2.1M1.47x6.33x
takeWhile->map1,000152.6k1.1M191.1k161.6k57.6k-157.4k0.80x5.56x
takeWhile->map100,0001.1k3.6k1.4k1.0k428-1.2k0.77x2.61x
flatMap->filter101.8M27.0M1.9M1.9M838.0k-1.1M0.95x14.1x
flatMap->filter1,00020.8k295.7k21.2k23.3k8.8k-14.2k0.89x12.7x
flatMap->filter100,00014429713614778-1090.98x2.02x
map->sortBy->take(10)102.7M4.7M2.6M2.0M973.5k3.9M1.4M0.69x1.20x
map->sortBy->take(10)1,0009.8k471.8k10.3k9.9k8.4k31.2k9.9k0.32x15.1x
map->sortBy->take(10)100,000443.7k44443889440.49x41.4x
map->uniq->filter104.0M5.9M3.0M1.9M1.1M3.9M1.8M1.04x1.53x
map->uniq->filter1,00053.1k62.8k46.4k22.1k12.3k13.3k49.1k1.08x1.28x
map->uniq->filter100,0001811771591288621761.03x1.00x
scan->map107.7M10.0M4.5M----1.72x2.23x
scan->map1,000116.9k142.1k81.9k----1.43x1.73x
scan->map100,0008491.1k624----1.36x1.79x
heavy map->filter->reduce10954.4k1.2M935.2k852.9k617.3k918.3k736.3k1.02x1.27x
heavy map->filter->reduce1,0007.4k8.8k7.6k7.4k5.6k7.4k7.3k0.98x1.16x
heavy map->filter->reduce100,000688669685168691.00x1.25x
filter->head1010.1M43.4M9.2M6.8M4.1M8.9M2.5M1.10x4.73x
filter->head1,000224.0k43.5M248.0k243.4k4.0M252.9k188.6k0.06x10.8x
filter->head100,0002.3k43.4M2.5k2.5k3.9M2.9k2.1k0.00x11.2x

Compiled wins every row. Typical chains land 2-15x over the fastest competitor. Chains that stop early (find, take, head) at n >= 1,000 jump to 41-174x, because the fused loop never touches the rest of the array once it has its answer. Sort-then-take lands 15-41x, from the compiler’s top-k fusion on sortBy -> take(k).

Uncompiled pipe — plain function application, no build step — still beats ramda’s geomean by 1.85x on eager chains that consume the whole input, never dropping below 1.19x. It loses to lazy libraries only on chains that stop early at n >= 1,000 (see the find/take/head rows above): that’s what Iter is for, not the compiler.

Per-module comparisons. Each row is its own contract against its own frozen reference implementation, not the libraries above — geomeans from different rows aren’t comparable to each other. A ratio is reference time divided by Stopcock time: greater than 1 means Stopcock was faster for that row.

ContractPaired reference and casesBun/JSC geo / min
Build compiler, stratifiedFrozen loop emitter, 441.785x / 0.839x
Build compiler, operation-completeFrozen operation emitter, 138 timed + 2 optimizer canaries1.049x / 0.158x
Uncompiled pipe floor (invariant)ramda, 101.848x / 1.193x
Direct Iter terminalHand-written early-exit loop, 30.834x / 0.787x
Broad Iter surfaceFrozen executor, 141.509x / 0.182x
Array.withoutFrozen implementations, 271.974x / 0.938x
Typed arraysFrozen implementations, 488.653x / 0.373x
Typed arraysNative typed-array equivalents, 481.072x / 0.690x
pipe dispatchFrozen dispatcher, 41.081x / 1.018x
Core utilitiesFrozen implementations, 185.135x / 0.748x
Data and functional modulesFrozen implementations, 112.311x / 0.786x
Structural modulesFrozen implementations, 152.317x / 0.999x
Scalar, text, and hashFrozen implementations, 112.008x / 0.988x
Recursion, match, schema, writerFrozen implementations, 111.816x / 0.464x

A handful of rows carry a documented, evidenced floor below the shared default for specific cases — an early-exit or expansion shape the runtime engine’s removal intentionally stopped optimizing at the runtime level, a genuinely bimodal case across process runs, or an architectural gap against one frozen reference’s own strategy — rather than every case in that row clearing one blanket bar. See packages/fp/README.md and the comments in each gate under benchmarks/src/reference/ for the specific reasoning behind each one.

CI reruns the contracts with Bun 1.3.14 and Node 22 on Linux x64 and macOS arm64 and retains raw plus evaluated artifacts.

Terminal window
cd benchmarks
bun run bench # full benchmark suite
bun run perf:gates # release gates: compiled tier, size, correctness

The normal @stopcock/fp runtime stays compatible with CSP policies that omit unsafe-eval: no eval, new Function, function-source parsing, or dynamically loaded JIT in the public runtime graph. @stopcock/fp-compiler performs its rewrite during the build; it is not imported into the emitted application’s runtime graph.