Skip to content

Benchmarks

How neotraverse compares to the original traverse, across the full operation × shape matrix. Toggle between throughput (higher is better) and memory (lower is better).

Geometric mean speedup vs traverse: functional neotraverse ≈ 4.91×, neotraverse/legacy ≈ 2.97×.

Geometric mean allocation reduction on core walks (forEach, map, clone, reduce, paths, nodes): functional neotraverse ≈ 6.22× less, neotraverse/legacy ≈ 1.97× less (traverse B/op ÷ neotraverse B/op).

These numbers come from packages/neotraverse/bench/run.ts (powered by tinybench) and are imported directly from the committed bench/results.json. Reproduce locally with pnpm bench.

Generated 2026-06-08 · Node v24.1.0 · neotraverse 1.0.0 vs traverse 0.6.11

These are runtime benchmarks.

Bundle size (tree-shaken, brotli)

The default neotraverse export is utility-first (sideEffects: false): your bundler drops unused exports.

ScenarioBrotli (approx.)
One walk terminal (forEach, map, find, …)~1.9 KB
Path helpers only (get / has / set)~0.3 KB
All functions except deprecated Traverse~5.7 KB

Range: ~2–6 KB brotli (tree-shaken), floor ≈ single traversal, ceiling ≈ full toolkit.

Source: bench/bundle-sizes.json (pnpm bundle-size in packages/neotraverse). For a third-party traverse vs neotraverse comparison, see bundle-roast ↗.

forEach

small, { a, b, c:[…] } — the canonical example
traverse841K
legacy2.41M · 2.87×
modern4.02M · 4.78× 🏆
wide, flat object with 64 keys
traverse79.3K
legacy284K · 3.58×
modern594K · 7.49× 🏆
deep, 32-level deeply nested object
traverse53.2K
legacy160K · 3.01×
modern331K · 6.22× 🏆
array, array of 256 small objects
traverse3.0K
legacy10.2K · 3.37×
modern21.2K · 7.04× 🏆
json, realistic nested API response
traverse57.4K
legacy187K · 3.25×
modern382K · 6.66× 🏆

map

small, { a, b, c:[…] } — the canonical example
traverse393K
legacy1.27M · 3.22×
modern2.27M · 5.76× 🏆
wide, flat object with 64 keys
traverse50.5K
legacy106K · 2.11×
modern184K · 3.64× 🏆
deep, 32-level deeply nested object
traverse18.8K
legacy87.2K · 4.63×
modern137K · 7.25× 🏆
array, array of 256 small objects
traverse1.4K
legacy4.4K · 3.08×
modern8.6K · 6.07× 🏆
json, realistic nested API response
traverse23.2K
legacy84.0K · 3.62×
modern151K · 6.52× 🏆

clone

small, { a, b, c:[…] } — the canonical example
traverse494K
legacy3.36M · 6.79×
modern4.88M · 9.88× 🏆
wide, flat object with 64 keys
traverse210K
legacy309K · 1.47×
modern574K · 2.74× 🏆
deep, 32-level deeply nested object
traverse27.9K
legacy171K · 6.13×
modern260K · 9.35× 🏆
array, array of 256 small objects
traverse3.3K
legacy11.4K · 3.51×
modern15.4K · 4.74× 🏆
json, realistic nested API response
traverse42.2K
legacy206K · 4.87×
modern303K · 7.18× 🏆

reduce

small, { a, b, c:[…] } — the canonical example
traverse797K
legacy2.20M · 2.76×
modern3.54M · 4.44× 🏆
wide, flat object with 64 keys
traverse75.6K
legacy260K · 3.44×
modern490K · 6.47× 🏆
deep, 32-level deeply nested object
traverse50.7K
legacy145K · 2.87×
modern289K · 5.7× 🏆
array, array of 256 small objects
traverse2.9K
legacy9.2K · 3.17×
modern17.8K · 6.11× 🏆
json, realistic nested API response
traverse55.3K
legacy174K · 3.15×
modern329K · 5.95× 🏆

paths

small, { a, b, c:[…] } — the canonical example
traverse812K
legacy2.28M · 2.81×
modern3.40M · 4.19× 🏆
wide, flat object with 64 keys
traverse77.4K
legacy268K · 3.46×
modern475K · 6.14× 🏆
deep, 32-level deeply nested object
traverse51.0K
legacy152K · 2.98×
modern213K · 4.17× 🏆
array, array of 256 small objects
traverse3.0K
legacy9.6K · 3.21×
modern16.4K · 5.5× 🏆
json, realistic nested API response
traverse56.3K
legacy177K · 3.15×
modern295K · 5.24× 🏆

nodes

small, { a, b, c:[…] } — the canonical example
traverse801K
legacy2.26M · 2.82×
modern3.49M · 4.36× 🏆
wide, flat object with 64 keys
traverse76.6K
legacy265K · 3.46×
modern507K · 6.61× 🏆
deep, 32-level deeply nested object
traverse51.2K
legacy150K · 2.93×
modern288K · 5.63× 🏆
array, array of 256 small objects
traverse2.9K
legacy9.4K · 3.21×
modern17.7K · 6.02× 🏆
json, realistic nested API response
traverse55.5K
legacy177K · 3.18×
modern328K · 5.91× 🏆

get

json, realistic nested API response
traverse18.1M
legacy22.2M · 1.23× 🏆
modern21.3M · 1.18×

has

json, realistic nested API response
traverse18.9M
legacy22.3M · 1.17× 🏆
modern22.2M · 1.17×

set

json, realistic nested API response
traverse23.6M
legacy23.9M · 1.01× 🏆
modern19.4M · 0.82×

Notes

  • Traversal operations (forEach, map, clone, reduce, paths, nodes) on the functional default neotraverse export average ~5.6× vs traverse and peak at ~10× (clone · small), with ~5.7× less heap per op on average (up to ~11× on forEach · wide). The neotraverse/legacy drop-in averages ~3× speed and ~2× less allocation across core walks.
  • clone peaks at ~10× on the functional API (clone · small). The legacy build and the functional API share the same clone() / copy() source, but the functional bundle inlines a leaner walk, so it often wins by a wide margin on smaller shapes while wide flat objects stay closer (~3×).
  • The get / has / set path helpers are micro-ops (20M+ ops/s); the functional neotraverse API and the neotraverse/legacy drop-in are both within noise of traverse. The legacy class stores its state in plain instance fields (not #private, which would downlevel to WeakMaps at ES2015), so it stays native-fast here too.
  • Memory figures are an approximate bytes-allocated-per-op signal (median of GC-bracketed samples). JS memory measurement is noisy, treat them as ballpark, like the throughput margins of error (rme) in the JSON.

neotraverse/safe — the safety trade-off

neotraverse/safe is a separate, opt-in core built on an iterative engine. It is not a faster successor — it trades a little throughput for stack-safety and bounded memory on partial consumption. The point of these numbers is when to reach for it, not "/safe is faster".

Throughput

On a full eager scan, /safe's lazy iterator runs at roughly 0.8× the default neotraverse (which is itself ~5× faster than traverse) — so /safe is still ~4× faster than the original traverse. clone is at parity; in-place set is slightly ahead. Reproduce with pnpm bench:safe (written to bench/results-safe.json).

Memory & stack-safety (the reasons it exists)

Reproduce with pnpm bench:safe-memory · 100,000-node tree · Node v24.1.0

Scenariodefault neotraverseneotraverse/safeVerdict
Deep input (linked tree, levels)overflows past ~2,000 💥200,000+, no overflow/safe only
First 5 of a filtered scan (peak MB)19.4 MB3.1 MB/safe 6.3× less
Materialize the whole tree (peak MB)42.6 MB77 MBdefault wins (1.8× more)

Read this honestly:

  • Stack safety is categorical. The default walk is recursive and overflows on deep input; /safe is iterative and doesn't. For untrusted JSON or naturally deep data (ASTs, file trees), this is a correctness/security difference, not a speed one.
  • Early-exit / streaming uses far less memory — because /safe's lazy chains never materialize the intermediate collection that the default's array-returning filter/paths/nodes must build first. This win is algorithmic, so it's stable.
  • Full materialization is not a memory win. /safe allocates one Visit per node, so keeping the whole tree costs more. Its memory advantage is specifically about not materializing what you don't consume.

See Why /safe for the full story.

Released under the MIT License.

157.47Mtotal npm downloadssince Dec 2024