# neotraverse > Traverse and transform objects by visiting every node on a recursive walk: zero-dependency, hardened, ~5× faster and ~6× leaner with the functional API (up to ~10× / ~11×). Drop-in replacement for `traverse`. Docs: https://neotraverse.puruvj.dev · npm: https://www.npmjs.com/package/neotraverse --- # neotraverse Traverse and transform objects by visiting every node on a recursive walk. A TypeScript rewrite of [`traverse`](https://github.com/ljharb/js-traverse) with **0 dependencies**, **prototype-pollution hardening**, and **~5× the throughput** with the functional API (up to **~10×** on core walks) and **~6× less allocation** per op. Import **only what you use** from `neotraverse` (`sideEffects: false`). Named imports like `import { forEach } from 'neotraverse'` pull in one walk; importing every function is the upper bound. See [Bundle size (brotli)](#bundle-size-brotli) below. **Coming from `traverse`?** See [**Differences from traverse**](/guide/vs-traverse) (drop-in vs functional) or the [**Legacy / Classic API**](/legacy) for the `this`-bound reference. - 🤌 **~2 to 6 KB brotli** (tree-shaken functional API; see [bundle range](#bundle-size-brotli)) - 🚥 Zero dependencies, no polyfills - 🎹 Types included: drop `@types/traverse` - 🛡️ Safe on untrusted input (see [Security](/guide/security)) - ⚡ ~5× faster and ~6× leaner than `traverse` with the functional API, up to ~10× / ~11× (see [benchmarks](/benchmarks)) - 🧰 Query helpers, lazy iteration, async traversal, `Map`/`Set` clone ## Install ```sh npm install neotraverse # or: pnpm add neotraverse / bun add neotraverse / yarn add neotraverse ``` ## Quick start ```ts import * as t from 'neotraverse'; const obj = { a: 1, b: 2, c: [3, 4] }; t.forEach(obj, (ctx, x) => { if (typeof x === 'number') ctx.update(x * 10); }); // → { a: 10, b: 20, c: [30, 40] } ``` Named imports work too (`import { forEach, clone } from 'neotraverse'`) when you only need a few ops. ## Functional API Each `t` method call is a **terminal** operation: one walk per invocation. There is no `pipe()` helper; tree-to-tree ops such as `t.map` and `t.clone` compose as plain nested calls (`t.clone(t.map(obj, cb))`). `t.reduce(obj, cb)` is **seedless**: the accumulator starts at the root and the root node is skipped. Pass an explicit initial value as the third argument for a seeded fold: `t.reduce(obj, cb, 0)`. Seedless calls cannot also pass options positionally; pass an explicit seed (for example `undefined`) if you need options. ## Bundle size (brotli) Sizes are **brotli** after your bundler minifies and tree-shakes `neotraverse` (measured with esbuild; see [`bench/bundle-sizes.json`](https://github.com/PuruVJ/neotraverse/blob/main/packages/neotraverse/bench/bundle-sizes.json)). Reproduce with `pnpm bundle-size` in `packages/neotraverse`. | What you import | Brotli (approx.) | Notes | | ----------------------------------------------------------- | ------------------ | ------------------------------------------------------ | | **One walk terminal** (`forEach`, `map`, `find`, `size`, …) | **~2 KB** | Same ballpark for any single DFS callback op | | **Path helpers only** (`get` / `has` / `set`, or `getPath`) | **~0.3 to 0.5 KB** | No full-tree walk, keyed access / parse only | | **`clone` only** | **~0.9 KB** | Deep copy without installing the walk callback surface | | **All functions** | **~5.8 KB** | Upper bound when you use the full toolkit | **Range: ~2 to 6 KB brotli**, floor is one traversal (`forEach`-class import), ceiling is the full function surface. Pulling in every function without tree-shaking is ~5.8 KB brotli, same as “all functions”. Use **named imports** so dead code drops out. ## Documentation map **Getting started** - [Differences from traverse](/guide/vs-traverse): drop-in vs functional, what's new - [Options](/guide/options): `immutable`, `maxDepth`, `signal`, `descendIntoMapSet`, `concurrency` - [Security](/guide/security): prototype pollution, injection, DoS guard **Concepts** - [Types & traversal](/guide/types): `getType()` matrix, JSON-like trees, Map/Set behaviour - [Context](/guide/context): `ctx` reference, `block`, siblings **API reference** (reference + examples on each page) - [Core](/guide/api/core): `map`, `forEach`, `reduce`, `paths`, `nodes`, `clone`, `get` / `set` / `has` - [Paths & metrics](/guide/api/paths): string paths, `findPaths`, `select`, `count`, `getType` - [Structural](/guide/api/structural): `deleteWhere`, `prune`, `deepEqual`, `toJSON`, `freeze`, `diff`, `patch` - [Walk variants](/guide/api/walk): `walk`, BFS, `skipWhere`, `groupBy`, `merge`, `dereference` - [Query](/guide/api/query): `find`, `filter`, `some`, `every` - [Iteration](/guide/api/iteration): `entries`, `values`, Map/Set leaves - [Async](/guide/api/async): `forEachAsync`, `mapAsync`, concurrency, `AbortSignal` **More** - [Legacy / Classic API](/legacy) - [Migrating from traverse](/migration) - [Benchmarks](/benchmarks) ## Example index Runnable snippets live next to each API on the pages above: | Recipe | Page | | --------------------------------------- | ------------------------------------------------------- | | In-place `forEach` (negatives → offset) | [Core → forEach](/guide/api/core#forEach) | | Immutable `map` | [Core → map](/guide/api/core#map) | | Scrub circular refs | [Core → map](/guide/api/core#map) | | Dot / JSON Pointer paths | [Paths → getPath](/guide/api/paths#getPath) | | Find / filter paths | [Paths → findPaths](/guide/api/paths#findPaths) | | Redact secrets | [Structural → deleteWhere](/guide/api/structural#prune) | | Freeze snapshot | [Structural → freeze](/guide/api/structural#freeze) | | Cycle-safe JSON | [Structural → toJSON](/guide/api/structural#toJSON) | | Diff / patch | [Structural → diff](/guide/api/structural#diff) | | Leaf `filter` / `reduce` sum | [Query](/guide/api/query) | | `block` / skip subtree | [Context](/guide/context) | | `getType` branching | [Walk](/guide/api/walk) | | Async translate | [Async](/guide/api/async) | ## Builds & browser support The default `neotraverse` (functional) build is **ES2022** (Chrome/Edge 94+, Firefox 93+, Safari 15+, Node 18+, Deno, Bun). For the classic `this`-bound API and an ES2015 build for older targets, see the [**Legacy / Classic API**](/legacy). ## Migrating from `traverse` Start with [**Differences from traverse**](/guide/vs-traverse), then the [**Migration guide**](/migration) for install steps, side-by-side diffs, and the class→function table. ## License [MIT](https://github.com/PuruVJ/neotraverse/blob/main/packages/neotraverse/LICENSE), [Puru Vijay](https://puruvj.dev). --- # Differences from `traverse` [`traverse`](https://github.com/ljharb/js-traverse) is the classic `this`-bound walker. **neotraverse** keeps that API as a drop-in (now at `neotraverse/legacy`), ships a **utility-first functional** API as the default `neotraverse` export, and hardens the engine for real-world JSON and config trees. **Stay on `traverse` syntax?** `import traverse from 'neotraverse/legacy'`, one-line swap. **Starting fresh or want tree-shaking?** `import { forEach } from 'neotraverse'`. **Step-by-step upgrade?** See [Migrating from traverse](/migration). ## At a glance | | `traverse` | **neotraverse** | | ------------------------ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **Dependencies** | Ships with runtime deps | **Zero**, no polyfills | | **Types** | `@types/traverse` | **Built in** | | **Untrusted JSON** | Classic behaviour | **Hardened**, pollution & injection safe ([Security](/guide/security)) | | **Throughput** | Baseline | **~3×** drop-in (`neotraverse/legacy`) · **~5×** functional API (`neotraverse`, up to **~10×** on core walks, [Benchmarks](/benchmarks)) | | **Memory / walk** | Baseline | **~2×** less allocation (drop-in) · **~6×** less (functional, up to **~11×** on wide `forEach`, [Benchmarks](/benchmarks)) | | **Bundle (functional)** | Monolithic import | **Tree-shakeable** (`sideEffects: false`), [~2 to 6 KB brotli](/guide#bundle-size-brotli) for typical apps | | **Default API** | `traverse(obj).forEach(fn)` | **Functional** on `neotraverse`; classic drop-in on `neotraverse/legacy` | | **Recommended new code** | - | `neotraverse`, `forEach(obj, (ctx, x) => …)` | ## What stays the same On the **legacy** build, the mental model is unchanged: - `traverse(obj)` returns an instance with `.forEach`, `.map`, `.reduce`, `.paths`, `.nodes`, `.clone`, `.get`, `.set`, `.has` - Callbacks use **`this`** as the traversal context (`this.update`, `this.path`, `this.isLeaf`, …) - Options and return shapes match what you already know from `traverse` ```ts import traverse from 'neotraverse/legacy'; traverse({ a: 1, b: 2 }).forEach(function (x) { if (typeof x === 'number') this.update(x * 10); }); ``` ## What changes (even as a drop-in) You get these wins **without** rewriting callbacks: | Change | Why it matters | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | **No `@types/traverse`** | Types ship with the package | | **Prototype-pollution safety** | `clone` / `map` / `set` refuse hostile `__proto__` keys ([details](/guide/security)) | | **Faster, leaner walks** | Same call shape, higher ops/sec and ~2× less heap per op ([benchmarks](/benchmarks)) | | **Drop-in lives at `/legacy`** | `import traverse from 'neotraverse/legacy'` (ES2015, CJS + ESM); the classic `this`-bound API stays behaviour-compatible with `traverse` | ## The functional API (default `neotraverse` export) This is the biggest _optional_ difference, not required to migrate, but what most new projects should use. It is the recommended entry point. ### Callback shape: `this` → `ctx` | | `traverse` / `neotraverse/legacy` | `neotraverse` (functional) | | ------- | --------------------------------- | ----------------------------------- | | Style | `function (x) { this.update(…) }` | `(ctx, x) => { ctx.update(…) }` | | Context | `this` | First argument `ctx` | | Imports | Default export, chained methods | **Named** functions, tree-shakeable | ```ts import { forEach } from 'neotraverse'; forEach({ a: 1, b: 2 }, (ctx, x) => { if (typeof x === 'number') ctx.update(x * 10); }); ``` Every `this.*` field has the same name on `ctx` ([Context reference](/guide/context)). ### Options move to the last argument ```ts // traverse traverse(obj, { immutable: true }).map(fn); // functional import { map } from 'neotraverse'; map(obj, fn, { immutable: true }); ``` ### Class API is deprecated `new Traverse(obj)` still works in **1.0** but is removed in **v2**. It lives at `neotraverse/modern` and is trimmed to the legacy method set only. Prefer the named functions from the default `neotraverse` export. ## Only on the functional API (no `traverse` equivalent) These are **additive**, your old code keeps working; you opt in when you need them. | Area | Examples | | ------------------ | ------------------------------------------------------------------------------------- | | **String paths** | `getPath`, `setPath`, `hasPath`, `parseDotPath`, `parseJsonPointer` | | **Path search** | `findPaths`, `filterPaths`, `select` (glob), `count`, `size` | | **Structural** | `deleteWhere`, `prune`, `pruneDeep`, `deepEqual`, `toJSON`, `freeze`, `diff`, `patch` | | **Walk control** | `walk`, `breadthFirst`, `mapBfs`, `skipWhere`, `groupBy`, `merge`, `dereference` | | **Typing** | `getType`, explicit node kinds (object, array, Map, Set, …) | | **Lazy iteration** | `entries`, `values` generators | | **Async** | `forEachAsync`, `mapAsync` with `concurrency` and `AbortSignal` | | **Map / Set** | `descendIntoMapSet`, traverse collection entries, not just object keys | Full reference: [API docs](/guide/api/core) · [Example index](/guide#example-index). ## Three ways to adopt ```sh [1. Swap the package] npm install neotraverse npm uninstall traverse @types/traverse ``` ```diff [2. Change the import] -import traverse from 'traverse'; +import traverse from 'neotraverse/legacy'; ``` ```js [3. Or alias in the bundler: zero source edits] // vite.config.js export default { resolve: { alias: { traverse: 'neotraverse/legacy' } } }; ``` ## Which build should I use? | Build | Import | Use when | | ------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------ | | **Default (functional)** | `neotraverse` | New apps, TypeScript, tree-shaking, extra helpers, fastest `get`/`set`; the recommended entry | | **Deprecated class** | `neotraverse/modern` | Only for existing `Traverse` class users; deprecated, removed in v2 | | **Legacy** | `neotraverse/legacy` | Drop-in replacement; `this`-bound API; older bundlers / CommonJS; still `traverse`-compatible (ES2015) | ## What you keep from `traverse` - Deep walks, in-place updates, immutable `map`, `reduce`, path helpers on the instance - The same context vocabulary (`update`, `delete`, `remove`, `block`, `skip`, keys, parents, circular handling) - Familiar ergonomics on the legacy build, [Legacy / Classic API](/legacy) ## Next steps - [**Migrating from traverse**](/migration): install, diff, class→function table, removal timeline - [**Introduction**](/guide): quick start, bundle range, documentation map - [**Security**](/guide/security): untrusted JSON, `maxDepth` - [**Benchmarks**](/benchmarks): interactive charts vs `traverse` --- # Options Pass options as the **last** argument (for example `t.forEach(obj, cb, { maxDepth: 100 })`). `t.map` and `t.mapAsync` always run immutably; other ops default to in-place mutation unless you set `immutable: true`. ```ts { immutable: false, // if true, never mutate the original object includeSymbols: false, // if true, also traverse own enumerable symbol keys maxDepth: undefined, // bound recursion depth (throws RangeError when exceeded) signal: undefined, // AbortSignal, cancels forEachAsync()/mapAsync() descendIntoMapSet: false, // if true, visit Map/Set entries during walks (see Iteration) concurrency: 1, // parallel sibling callbacks in forEachAsync/mapAsync (see Async) } ``` See also [Security](/guide/security) (`maxDepth` DoS guard) and [Types & traversal](/guide/types) (`includeSymbols`, `descendIntoMapSet`). ### Example ```ts import * as t from 'neotraverse'; t.forEach( untrusted, (ctx, x) => { /* … */ }, { maxDepth: 500, includeSymbols: false, immutable: true } ); ``` --- # Security `neotraverse` is designed to be safe to run on **untrusted data**. - **No prototype pollution.** `set(path, value)` refuses to navigate or write through `__proto__`, `constructor`, or `prototype`, so an attacker-controlled path can't reach `Object.prototype`. - **No prototype injection.** `clone()`, `map()`, and `forEach()` assign keys without ever triggering the `__proto__` setter. An object parsed from hostile JSON such as `{"__proto__":{"isAdmin":true}}` is cloned with its real prototype intact, `result.isAdmin` is `undefined`, and the injected value is preserved as an inert own data property. - **Prototype preservation still works.** Legitimate class instances keep their prototype (`instanceof` is unaffected) after `clone()`/`map()`. - **No prototype-chain disclosure.** `get()` and `has()` only ever follow **own** properties. ### Example ```ts import * as t from 'neotraverse'; const evil = JSON.parse('{"user":"bob","__proto__":{"isAdmin":true}}'); const safe = t.clone(evil); safe.isAdmin; // undefined, not polluted Object.getPrototypeOf(safe); // Object.prototype ({}).isAdmin; // undefined, global prototype untouched ``` Read the story of the audit that produced these guarantees in the [**1.0 release post**](https://puruvj.dev/blog/neotraverse-1-0). ## DoS guard: `maxDepth` Traversal is recursive, so a deeply-nested hostile object can overflow the stack. Pass `maxDepth` to bound it; a catchable `RangeError` is thrown before the native overflow. Unlimited when omitted (default behaviour is unchanged). See [Options](/guide/options). ### Example ```ts import * as t from 'neotraverse'; try { t.clone(untrusted, { maxDepth: 1000 }); } catch (e) { // RangeError: neotraverse: maximum traversal depth (1000) exceeded } ``` --- # Types and traversal How neotraverse treats different JavaScript values. Use `t.getType(node)` inside callbacks; use `typeof` when you need finer detail inside the `'primitive'` bucket (`'string'` vs `'number'`, etc.). ## JSON-like trees (the common case) If your data looks like **`JSON.parse` output**, plain objects, arrays, `null`, strings, numbers, booleans, and nested combinations, you are on the supported path: - **`forEach` / `map` / `reduce`** visit every own enumerable property. - **`clone`** deep-copies structure and handles circular references. - **`get` / `set` / `has`** and **`getPath`** follow own keys only (no prototype chain). - **`includeSymbols: false`** (default) matches JSON (no symbol keys). Typical API/config/document trees need nothing special. Add **`Date`**, **`RegExp`**, **`Map`**, or **`Set`** when your runtime actually uses them; see the table below for walk vs clone differences. ## Full type reference | Kind | `getType()` | `forEach` / `map` walk | `clone()` | Notes | | -------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------- | | `null` | `'null'` | Visited | Copied | | | `string`, `number`, `boolean`, `bigint`, `symbol`, `undefined` | `'primitive'` | Visited as values | Copied | Use `typeof` to branch | | `function` | `'function'` | Node value only; not invoked | Same reference on properties | Stripped by `toJSON()` | | Plain object, class instance | `'object'` | Descends into **own enumerable** keys | Deep clone; keeps prototype | Boxed `new String()` etc. are `'object'` **leaves** | | `Array` | `'array'` | Descends indices | Deep clone | | | `Date` | `'date'` | Leaf (no keys) | Cloned via `getTime()` | | | `RegExp` | `'regexp'` | Leaf | Cloned | | | `Map` | `'map'` | **Leaf** by default; with `{ descendIntoMapSet: true }` visits each **value** at its key | **Deep-clones entries** | See [Iteration](/guide/api/iteration) | | `Set` | `'set'` | **Leaf** by default; with `{ descendIntoMapSet: true }` visits each element at a numeric index | **Deep-clones values** | Same as `Map` | | `WeakMap` / `WeakSet` | `'weakmap'` / `'weakset'` | **Leaf** (not enumerable in walk) | **`clone` / `copy` return the same reference** | Cannot query weak refs after GC | | Typed array (`Uint8Array`, …) | `'typed-array'` | Descends index keys; `copy` uses `.slice()` | Leaf in deep clone (buffer copied) | | | `ArrayBuffer` | `'arraybuffer'` | Leaf | `.slice(0)` | | | `DataView` | `'dataview'` | Leaf | Clones viewed byte range | | | `Error` | `'error'` | Leaf | Deep clone; shallow `map` copy is lossy (`message` only) | `deepEqual`: `message` + `name` | | `Promise`, `URL`, DOM nodes, … | `'object'` | Own enumerable keys only, if any | Generic object copy | Host objects may behave oddly | ## Deliberate limits - **No prototype walking**, only own properties (`get`/`has` never follow the chain). Safer on untrusted input. - **No non-enumerable / getter-only keys** unless you add your own logic. - **`Map` / `Set` vs walk**, by default the walker sees the collection as one node; pass **`descendIntoMapSet: true`** to visit entries. **`clone`** always deep-copies entries regardless. - **Boxed primitives** (`new String('x')`), classified as `'object'`, treated as **leaves**; unwrap with `.valueOf()` when needed. - **`diff` / `patch`**, acyclic trees only; circular graphs are unsupported. - **Cross-realm**, `instanceof` may fail; `copy()` falls back to tag checks for `Date` / `RegExp`. ## Quick checks in callbacks ### Example ```ts import * as t from 'neotraverse'; t.forEach(data, (ctx, x) => { switch (t.getType(x)) { case 'primitive': if (typeof x === 'string') { /* … */ } break; case 'function': // present on the tree, but not called by neotraverse break; case 'map': case 'set': // walk leaf, use t.clone(x), descendIntoMapSet, or iterate x yourself break; case 'arraybuffer': case 'dataview': case 'typed-array': // binary data; clone copies bytes break; default: break; } }); ``` For transforming by kind in a single pass, see [Walk variants → getType](/guide/api/walk#getType). --- # Context Every callback receives a context (`ctx` argument): | Property | Description | | --------------------------------- | ----------------------------------------------------------------------------- | | `node` | The present node. | | `path` | Array of keys from the root to the present node. | | `parent` / `parents` | The parent context / all ancestor contexts. | | `key` | The key of the present node in its parent (`undefined` at the root). | | `isRoot` / `notRoot` | Whether the node is the root. | | `isLeaf` / `notLeaf` | Whether the node has no children. | | `isFirst` / `isLast` | Whether the node is the first / last sibling (stale in the current callback). | | `level` | Depth of the node within the traversal. | | `circular` | The ancestor context this node is a cycle back to, if any. | | `update(value, stopHere?)` | Set a new value for the node. Stops descending when `stopHere` is true. | | `remove(stopHere?)` | Remove from the output (spliced from arrays, deleted otherwise). | | `delete(stopHere?)` | `delete` from the parent (even on arrays). | | `keys` | The node's keys, assign in `before()` to traverse in a custom order. | | `before(fn)` / `after(fn)` | Run before / after all children are traversed. | | `pre(fn)` / `post(fn)` | Run before / after **each** child is traversed. | | `stop()` | Stop the entire traversal. | | `block()` | Don't descend into the current node's children. | | `nextSibling()` / `prevSibling()` | Lightweight sibling context (path snapshot). See below. | Import as `import * as t from 'neotraverse'`. API methods that take callbacks receive this [context](/guide/context). ## Skip a subtree with `block` `ctx.block()` prevents visiting descendants of the current node. Equivalent to using [`t.skipWhere`](/guide/api/walk#skipWhere) in a callback. ### Example ```ts import * as t from 'neotraverse'; t.forEach(config, (ctx) => { if (ctx.key === 'skipMe') ctx.block(); if (typeof ctx.node === 'number') ctx.update(ctx.node * 2); }); // nodes under `skipMe` are not visited or updated ``` ## Circular references When a node equals an ancestor, `ctx.circular` points at that ancestor and the walker does not descend further. ### Example ```ts import * as t from 'neotraverse'; const obj = { a: 1, b: 2, c: [3, 4] }; obj.c.push(obj); const scrubbed = t.map(obj, (ctx) => { if (ctx.circular) ctx.remove(); }); // → { a: 1, b: 2, c: [ 3, 4 ] } ``` See also [Structural → toJSON](/guide/api/structural#toJSON) for logging cyclic graphs. ## Sibling helpers: `nextSibling` · `prevSibling` Return a lightweight sibling context (path snapshot only, not a full re-walk). Reads live `parent.keys`; unlike `isFirst` / `isLast`, safe when you need the adjacent key's node. ### Example ```ts import * as t from 'neotraverse'; const obj = { a: 1, b: 2, c: 3 }; t.forEach(obj, (ctx) => { if (ctx.key === 'b') { const prev = ctx.prevSibling(); const next = ctx.nextSibling(); // prev?.node === 1, next?.node === 3 } }); ``` --- # Core traversal Import as `import * as t from 'neotraverse'`. Each callback receives the [context](/guide/context). ## map `t.map(obj, fn, options?)` runs `fn` for each node and returns a **new** object. Update nodes in the result with `ctx.update(value)`. ### Example ```ts import * as t from 'neotraverse'; const input = { a: 1, nested: { b: 2 } }; const out = t.map(input, (ctx, x) => { if (typeof x === 'number') ctx.update(x * 10); }); // out → { a: 10, nested: { b: 20 } } // input is unchanged ``` ### Example: scrub circular references ```ts import * as t from 'neotraverse'; const obj = { a: 1, b: 2, c: [3, 4] }; obj.c.push(obj); const scrubbed = t.map(obj, (ctx) => { if (ctx.circular) ctx.remove(); }); // → { a: 1, b: 2, c: [ 3, 4 ] } ``` ## forEach `t.forEach(obj, fn, options?)` is like `t.map`, but `ctx.update()` mutates `obj` **in place** (returns the same reference). ### Example ```ts import * as t from 'neotraverse'; const obj = [5, 6, -3, [7, 8, -2, 1], { f: 10, g: -13 }]; t.forEach(obj, (ctx, x) => { if (x < 0) ctx.update(x + 128); }); // → [ 5, 6, 125, [ 7, 8, 126, 1 ], { f: 10, g: 115 } ] ``` ## reduce `t.reduce(obj, fn, init?, options?)` is a [left-fold]() over every node. Omit `init` to start from the root and skip the root node in the fold. See [Query → reduce](/guide/api/query#reduce) for a seeded example. ## paths · nodes `t.paths(obj, options?)` and `t.nodes(obj, options?)` return every non-cyclic path or every node. For lazy alternatives, see [Iteration](/guide/api/iteration). ## clone `t.clone(obj, options?)` deep-clones. Handles circular references, `Date`/`RegExp`/`Error`/typed arrays, and `Map`/`Set` (entries deep-cloned), and is prototype-pollution-safe. See [Types & traversal](/guide/types). ### Example ```ts import * as t from 'neotraverse'; const live = { count: 0 }; const snapshot = t.clone(live); snapshot.count = 99; live.count; // 0, unchanged ``` ## get · set · has `t.get(obj, path, options?)`, `t.set(obj, path, value, options?)`, and `t.has(obj, path, options?)` read / write / test at an array `path`. `get`/`has` only follow own properties; `set` refuses prototype-polluting keys. For string paths, see [Paths & metrics → getPath](/guide/api/paths#getPath). ### Example ```ts import * as t from 'neotraverse'; const obj = { a: { b: 1 } }; t.get(obj, ['a', 'b']); // 1 t.set(obj, ['a', 'b'], 2); t.has(obj, ['a', 'c']); // false ``` --- # Paths & metrics Import as `import * as t from 'neotraverse'`. ## findPaths · filterPaths Return **where** a match occurred, not only the value. `t.findPaths` stops at the first hit; `t.filterPaths` returns `{ path, node }[]`. ### Example ```ts import * as t from 'neotraverse'; const tree = { users: [{ id: 1 }, { id: 2, flag: true }] }; t.findPaths(tree, (_, x) => x === true); // ['users', '1', 'flag'] t.filterPaths(tree, (ctx) => ctx.isLeaf && typeof ctx.node === 'number'); // → [{ path: ['users','0','id'], node: 1 }, …] ``` ## getPath · setPath · hasPath String paths over array `get` / `set` / `has`. Dot notation (`a.b.0`) or JSON Pointer (`/a/b/0`). Unsafe segments (`__proto__`, etc.) throw at parse time. ### Example ```ts import * as t from 'neotraverse'; const config = { server: { host: 'localhost', port: 3000 } }; t.getPath(config, 'server.port'); // 3000 t.setPath(config, 'server.port', 8080); t.hasPath(config, 'server.tls'); // false // JSON Pointer (leading slash) t.getPath(config, '/server/host'); // 'localhost' after set ``` ## count · size `t.size(obj)` counts every visited node. `t.count(obj, fn)` counts nodes where the predicate is true. ### Example ```ts import * as t from 'neotraverse'; const tree = { a: 1, b: { c: 2 } }; t.size(tree); // 4 (root + a + b + c) t.count(tree, (_, x) => x === 2); // 1 ``` ## getType `t.getType(value)` returns a stable tag for branching inside callbacks. See [Types and traversal](/guide/types#types-and-traversal) for the full matrix. ## select Glob path query: `*`, `key[*]`, dot segments. For predicate search, use `t.filterPaths`. ### Example ```ts import * as t from 'neotraverse'; const data = { users: [{ email: 'a@x.com' }, { email: 'b@x.com' }] }; t.select(data, 'users[*].email'); // → [{ path: ['users','0','email'], node: 'a@x.com' }, …] ``` --- # Structural helpers Import as `import * as t from 'neotraverse'`. ## deleteWhere · prune `t.deleteWhere` returns a new tree with matching nodes removed (`map` + `ctx.remove()`). `t.prune` keeps nodes where the predicate is true (inverse). Map/Set stay whole leaves unless [descendIntoMapSet](/guide/options) is set. ### Example: redact secrets ```ts import * as t from 'neotraverse'; const apiPayload = { user: 'alice', token: 'secret', profile: { email: 'a@example.com', password: 'hunter2' } }; const safe = t.deleteWhere( structuredClone(apiPayload), (_, x) => x === 'secret' || x === 'hunter2' ); // drops matching nodes; original apiPayload unchanged if you cloned first ``` ## pruneDeep Replace nodes deeper than `maxDepth` with a sentinel (default `null`). Unlike the `maxDepth` **option**, this does not throw. ### Example ```ts import * as t from 'neotraverse'; const deep = { a: { b: { c: { d: 1 } } } }; t.pruneDeep(deep, 2); // → { a: { b: null } } (depth counted from root) ``` ## deepEqual Structural compare with an explicit per-type contract (Dates by time, RegExp by source+flags, etc.). Optional `compareFn` can override pairs. Not guaranteed to match `t.clone()` byte-for-byte (e.g. `Error` fields). ## toJSON `JSON.stringify` after a walk; circular references become `null` (configurable via `cycle`). ### Example ```ts import * as t from 'neotraverse'; const graph: any = { name: 'root' }; graph.self = graph; console.log(t.toJSON(graph)); // {"name":"root","self":null} ``` ## freeze Deep-freeze in place (children before parents). Pair with `t.clone` when you need an immutable snapshot. ### Example ```ts import * as t from 'neotraverse'; const snapshot = t.freeze(t.clone(liveConfig)); // snapshot is deeply frozen; liveConfig can still change ``` ## diff · patch RFC 6902 subset (`add` / `remove` / `replace`). `t.diff(a, b)` returns ops; `t.patch(clone(a), ops)` applies them. Acyclic trees only. ### Example ```ts import * as t from 'neotraverse'; const v1 = { title: 'Hi', items: [1, 2] }; const v2 = { title: 'Hello', items: [1, 3], extra: true }; const ops = t.diff(v1, v2); const v2FromV1 = t.patch(structuredClone(v1), ops); // v2FromV1 deep-equals v2 (acyclic trees) ``` --- # Walk variants Import as `import * as t from 'neotraverse'`. ## walk `t.walk(obj, cb, options?)` is the low-level depth-first walker behind `forEach` and `map`. It returns the (possibly mutated) root. Prefer `t.forEach` / `t.map` unless you need the same callback shape with a different return contract. ## breadthFirst · mapBfs `t.breadthFirst` and `t.mapBfs` traverse level-order. `breadthFirst` mutates in place like `forEach`; `mapBfs` clones first like `map`. Visit order differs from DFS, do not assume `isFirst` / `isLast` match sibling order in the queue. ### Example ```ts import * as t from 'neotraverse'; const obj = { a: { b: 1 }, c: 2 }; const order: string[] = []; t.breadthFirst(obj, (ctx) => { order.push(ctx.path.join('.') || '(root)'); }); // root first, then shallower keys before deeper ones (e.g. a, c before b) ``` ## skipWhere Predicate helper that calls `ctx.block()`, equivalent to `if (pred(ctx, v)) ctx.block()` inside your callback. Compose with other logic in a single pass. See also [Context → block](/guide/context#context-block). ### Example ```ts import * as t from 'neotraverse'; t.forEach(tree, (ctx, v) => { t.skipWhere((c) => c.level > 2)(ctx, v); if (typeof v === 'number') ctx.update(v * 2); }); ``` ## groupBy One walk; buckets every visited value: `Map`. Usually combine with `ctx.isLeaf` or `typeof` so the root object does not land in a bucket. ### Example ```ts import * as t from 'neotraverse'; const obj = { a: 1, b: 2, c: 3 }; const buckets = t.groupBy(obj, (ctx, v) => ctx.isLeaf && typeof v === 'number' ? (v % 2 === 0 ? 'even' : 'odd') : 'skip' ); buckets.get('odd'); // [1, 3] buckets.get('even'); // ``` ## merge `t.merge(target, source, options?)` returns a **new** tree (does not mutate `target`). Plain objects and `Map` entries merge recursively; arrays take indices from `source` (length follows `source`, default **`array: 'replace'`**). Use `{ array: 'concat' }` to append. ### Example ```ts import * as t from 'neotraverse'; const target = { a: { x: 1 }, arr: [1, 2] }; const source = { a: { y: 2 }, arr: [9] }; t.merge(target, source); // → { a: { x: 1, y: 2 }, arr: [9] } // target unchanged ``` ## dereference Resolve local JSON Pointer `$ref` objects (`{ "$ref": "#/definitions/Foo" }`) on a clone. External URL refs are left unchanged (`localOnly` defaults to `true`). ### Example ```ts import * as t from 'neotraverse'; const doc = { defs: { Foo: { type: 'string' } }, node: { $ref: '#/defs/Foo' } }; const out = t.dereference(doc); out.node; // { type: 'string' } ``` ## getType Use `t.getType(ctx.node)` inside `t.map` / `t.forEach`. Full matrix: [Types & traversal](/guide/types#types-and-traversal). ### Example ```ts import * as t from 'neotraverse'; t.map(doc, (ctx) => { switch (t.getType(ctx.node)) { case 'date': ctx.update(ctx.node.toISOString()); break; case 'map': case 'set': // Map/Set are walk leaves by default, clone handles entries break; } }); ``` --- # Query helpers Import as `import * as t from 'neotraverse'`. ## find · filter · some · every `t.find`, `t.filter`, `t.some`, and `t.every` search over every node (root included). `find`/`some` stop at the first match; `every` stops at the first failure. ### Example ```ts import * as t from 'neotraverse'; const tree = { a: 1, b: { c: 2, d: 3 } }; t.find(tree, (ctx, x) => x === 2); // 2 t.filter(tree, (ctx) => ctx.isLeaf); // [1, 2, 3] t.some(tree, (ctx, x) => x > 2); // true t.every(tree, (ctx, x) => typeof x !== 'string'); // true ``` ### Example: collect leaf nodes ```ts import * as t from 'neotraverse'; const leaves = t.filter({ a: [1, 2, 3], b: 4, d: { e: [7, 8], f: 9 } }, (ctx) => ctx.isLeaf); // → [ 1, 2, 3, 4, 7, 8, 9 ] ``` ## reduce `t.reduce(obj, fn, init?, options?)` folds over every node. See [Core → reduce](/guide/api/core#reduce) for seedless behaviour. ### Example: sum numbers ```ts import * as t from 'neotraverse'; const nested = { a: 1, b: { c: 2, d: 3 } }; const sum = t.reduce(nested, (_, acc, x) => (typeof x === 'number' ? acc + x : acc), 0); // 6 ``` --- # Lazy iteration Pull nodes without materializing `t.paths` / `t.nodes`. Circular references are visited once and not descended into. Import as `import * as t from 'neotraverse'`. ## values `for (const node of t.values(tree))` and `[...t.values(tree)]` yield every node depth-first (like `t.nodes()`, but lazy). ## entries `t.entries(obj, options?)` yields `[path, node]` pairs. ### Example ```ts import * as t from 'neotraverse'; const tree = { a: 1, b: { c: 2 } }; for (const node of t.values(tree)) { /* every node */ } for (const [path, node] of t.entries(tree)) { /* path: PropertyKey[], node: value at that path */ } ``` ## Map and Set By default, `t.forEach`, `t.map`, `t.paths`, `t.nodes`, and lazy iteration treat `Map`/`Set` as **leaf nodes**. Only `t.clone` (and the shallow `t.map` copy) descend into their entries. Pass **`descendIntoMapSet: true`** ([Options](/guide/options)) to visit Map values at their keys and Set elements at numeric indices during walks. ### Example: descend into Map entries ```ts import * as t from 'neotraverse'; const m = new Map([['k', { v: 1 }]]); const paths: PropertyKey[][] = []; for (const [path] of t.entries(m, { descendIntoMapSet: true })) { paths.push(path); } // includes paths into nested object values ``` See [Types & traversal](/guide/types#types-and-traversal) for walk vs clone behaviour. --- # Async traversal Import as `import * as t from 'neotraverse'`. ## forEachAsync · mapAsync The callback may be `async` and is awaited at each node. Pass `signal` in [options](/guide/options) to cancel via [`AbortController`](https://developer.mozilla.org/docs/Web/API/AbortController). `t.mapAsync` always runs immutably (like `t.map`). ### Example: async transform ```ts import * as t from 'neotraverse'; const doc = { title: 'Hello', body: 'world' }; const translated = await t.mapAsync(doc, async (ctx, x) => { if (typeof x === 'string') ctx.update(await translate(x)); }); ``` ### Example: abort ```ts import * as t from 'neotraverse'; const controller = new AbortController(); const walking = t.forEachAsync( big, async (ctx) => { /* … */ }, { signal: controller.signal } ); controller.abort(); // → `walking` rejects with the abort reason ``` ## Concurrency `forEachAsync` / `mapAsync` accept `{ concurrency: n }` (default `1`) to run up to **n** sibling callbacks in parallel. Each branch gets an isolated path/parent snapshot, intended for I/O-bound work; pair with `signal` to abort. ### Example ```ts import * as t from 'neotraverse'; const obj = { a: 0, b: 0, c: 0 }; await t.forEachAsync( obj, async (ctx) => { await fetch(`/api/item/${ctx.key}`); }, { concurrency: 3 } ); // up to three sibling fetches in flight at once ``` --- # Legacy / Classic API This is the original `traverse`-compatible API, a **drop-in replacement** for [`traverse`](https://github.com/ljharb/js-traverse). The traversal context is the callback's `this` binding (rather than a `ctx` argument). It ships as one build: - **`neotraverse/legacy`:** a CJS + ESM build targeting **ES2015**, the one-line swap for users of the original `traverse`. See [**Differences from traverse**](/guide/vs-traverse) for a full comparison. For new code, prefer the [**functional API**](/guide) (the default `neotraverse` export), the same engine, but tree-shakeable, with a `ctx` argument, query / iteration / async helpers, `Map`/`Set` clone, and the fastest path operations. This page documents the stable classic API, which stays a faithful `traverse` drop-in. ## Install ```sh npm install neotraverse npm uninstall traverse @types/traverse # types are built in now ``` ## Quick start The callback's `this` is the [context](#context): ```ts import traverse from 'neotraverse/legacy'; const obj = { a: 1, b: 2, c: [3, 4] }; traverse(obj).forEach(function (x) { if (typeof x === 'number') this.update(x * 10); }); // → { a: 10, b: 20, c: [30, 40] } ``` For CommonJS / older runtimes, require the same build (identical API): ```js const traverse = require('neotraverse/legacy'); ``` ## Build & browser support | Build | Import | Module | Target | Browsers | | ---------- | -------------------- | --------- | ------ | --------------------------------------------- | | **legacy** | `neotraverse/legacy` | CJS + ESM | ES2015 | Chrome 51+, Firefox 54+, Safari 10+, Edge 15+ | The legacy build now targets **ES2015** instead of ES5 (it is built with rolldown/oxc, whose floor is ES2015). It remains a CJS + ESM drop-in for `traverse`; only environments that required literal ES5 output, e.g. IE11, are no longer supported by the prebuilt bundle. ## Security The legacy build stays behaviour-compatible with the original `traverse`, so it intentionally does **not** carry the 1.0 security hardening (or the performance work). If you run on **untrusted data**, use the default [`neotraverse`](/guide) functional API, which refuses prototype-polluting keys, keeps the real prototype on `clone` / `map` of hostile JSON, and bounds recursion with `maxDepth`. ## Options ```ts traverse(obj, { immutable: false, // if true, never mutate the original object includeSymbols: false // if true, also traverse own enumerable symbol keys }); ``` > Hardening options like `maxDepth` and the async-only `signal` option live on the default [`neotraverse`](/guide/options) functional API. ## Methods Each method that takes an `fn` runs with the [context](#context) on `this`. ### `.map(fn)` Run `fn` for each node and return a **new** object with the results. Update nodes in the result with `this.update(value)`. ### `.forEach(fn)` Like `.map()`, but `update()` mutates the object **in place**. ### `.reduce(fn, acc)` A [left-fold]() over every node. If `acc` is omitted, it starts as the root object and the root node is skipped. ### `.paths()` Return an array of every non-cyclic path (each path an array of keys). ### `.nodes()` Return an array of every node. ### `.clone()` Create a deep clone. Handles circular references and `Date`/`RegExp`/`Error`/typed arrays. For prototype-pollution-safe cloning of untrusted data, use the default `neotraverse` functional API. ### `.get(path)` · `.set(path, value)` · `.has(path)` Read / write / test the element at an array `path`. > The query / iteration / async helpers (`find`, `filter`, `for…of`, `forEachAsync`, …) and `Map`/`Set` cloning live in the default [`neotraverse`](/guide/api/core) functional API. ## Context Every callback runs with the context bound to `this`: | Property | Description | | ------------------------------------ | ----------------------------------------------------------------------- | | `this.node` | The present node. | | `this.path` | Array of keys from the root to the present node. | | `this.parent` / `this.parents` | The parent context / all ancestor contexts. | | `this.key` | The key of the present node in its parent (`undefined` at the root). | | `this.isRoot` / `this.notRoot` | Whether the node is the root. | | `this.isLeaf` / `this.notLeaf` | Whether the node has no children. | | `this.isFirst` / `this.isLast` | Whether the node is the first / last sibling. | | `this.level` | Depth of the node within the traversal. | | `this.circular` | The ancestor context this node is a cycle back to, if any. | | `this.update(value, stopHere?)` | Set a new value for the node. Stops descending when `stopHere` is true. | | `this.remove(stopHere?)` | Remove from the output (spliced from arrays, deleted otherwise). | | `this.delete(stopHere?)` | `delete` from the parent (even on arrays). | | `this.keys` | The node's keys, assign in `before()` to traverse in a custom order. | | `this.before(fn)` / `this.after(fn)` | Run before / after all children are traversed. | | `this.pre(fn)` / `this.post(fn)` | Run before / after **each** child is traversed. | | `this.stop()` | Stop the entire traversal. | | `this.block()` | Don't descend into the current node's children. | ## Migrating from `traverse` Swap the import, that's the whole migration: ```diff -import traverse from 'traverse'; +import traverse from 'neotraverse/legacy'; ``` See the [**Migration guide**](/migration) for the full `traverse → neotraverse/legacy → neotraverse` path. ## License [MIT](https://github.com/PuruVJ/neotraverse/blob/main/packages/neotraverse/LICENSE), [Puru Vijay](https://puruvj.dev). --- # Migrating from `traverse` neotraverse is a **drop-in replacement** for [`traverse`](https://github.com/ljharb/js-traverse). You can adopt it in two steps and stop there, or take one more step to the faster, ergonomic **functional API** (the default `neotraverse` export). For a scannable comparison (same vs different, functional-only helpers, which entry to pick), read [**Differences from traverse**](/guide/vs-traverse) in Getting started. ## Step 1: install ```sh npm install neotraverse npm uninstall traverse @types/traverse # types are built in now ``` ## Step 2: swap the import (zero code changes) The legacy build keeps the exact `traverse` API (`this`-bound callbacks): ```diff -import traverse from 'traverse'; +import traverse from 'neotraverse/legacy'; traverse(obj).forEach(function (x) { if (x < 0) this.update(x + 128); }); ``` That's it. Same behaviour and zero-dependency. Full reference for this classic `this`-bound API: [**Legacy / Classic API**](/legacy). ## Step 3 (optional): go functional The functional API (the default `neotraverse` export) replaces the `this`-bound context with explicit helpers and arguments, nicer with arrow functions and TypeScript, tree-shakeable, and the fastest entry for path operations. ### The same task, three ways ```js [traverse (before)] import traverse from 'traverse'; traverse(obj).forEach(function (x) { if (typeof x === 'number') this.update(x * 10); }); ``` ```js [neotraverse/legacy (drop-in)] import traverse from 'neotraverse/legacy'; // identical to `traverse`, `this` is the context traverse(obj).forEach(function (x) { if (typeof x === 'number') this.update(x * 10); }); ``` ```js [neotraverse (functional)] import { map } from 'neotraverse'; // explicit ctx argument (great with arrow functions) const result = map(obj, (value, ctx) => { if (typeof value === 'number') ctx.update(value * 10); }); ``` | Concept | `traverse` / `neotraverse/legacy` | `neotraverse` (functional) | | ------------- | --------------------------------- | -------------------------- | | Callback | `function (x) { … }` | `(value, ctx) => { … }` | | Current value | `x` (and `this.node`) | `value` (and `ctx.node`) | | Update a node | `this.update(v)` | `ctx.update(v)` | | Is a leaf? | `this.isLeaf` | `ctx.isLeaf` | | Path | `this.path` | `ctx.path` | Every context member is identical; only the way you reach it changes. See the [context reference](/guide/context). The `Traverse` class at `neotraverse/modern` is **deprecated and will be removed in v2**. It now exposes only the legacy method set (`get`/`has`/`set`/`map`/`forEach`/`reduce`/`paths`/`nodes`/`clone`). Reach for the functional API (the default `neotraverse` export) instead. ## Old browsers / runtimes: `neotraverse/legacy` For ES2015 / CommonJS environments, import the legacy build (also a drop-in for `traverse`): ```js const traverse = require('neotraverse/legacy'); ``` ## Bundle-time aliasing (no code changes at all) Point `traverse` at `neotraverse/legacy` in your bundler and leave imports untouched, e.g. Vite: ```js // vite.config.js export default { resolve: { alias: { traverse: 'neotraverse/legacy' } } }; ``` ## New helpers (functional API) These ship from the default `neotraverse` export, e.g. `import * as t from 'neotraverse'`, no `traverse` equivalent. See the [example index](/guide#example-index) and [types & traversal](/guide/types#types-and-traversal). ## What you gain - 🛡️ [Prototype-pollution & injection safety](/guide/security) on untrusted input. - ⚡ [~5× the throughput](/benchmarks) and **~6× less allocation** with the functional API (up to ~10× / ~11×); ~3× speed and ~2× memory on the drop-in build. - 🤌 Zero dependencies, types included, ESM-first. --- # 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` ≈ {{ results.summary['neotraverse modern'] }}×**, **`neotraverse/legacy` ≈ {{ results.summary['neotraverse legacy'] }}×**. **Geometric mean allocation reduction on core walks** (`forEach`, `map`, `clone`, `reduce`, `paths`, `nodes`): **functional `neotraverse` ≈ {{ results.summary.memory.traversal['neotraverse modern'] }}× less**, **`neotraverse/legacy` ≈ {{ results.summary.memory.traversal['neotraverse legacy'] }}× less** (traverse B/op ÷ neotraverse B/op).
These numbers come from [`packages/neotraverse/bench/run.ts`](https://github.com/PuruVJ/neotraverse/blob/main/packages/neotraverse/bench/run.ts) (powered by [tinybench](https://github.com/tinylibs/tinybench)) and are imported directly from the committed `bench/results.json`. Reproduce locally with `pnpm bench`. Generated {{ date }} · Node {{ results.runtime.node }} · neotraverse {{ results.versions.neotraverse }} vs traverse {{ results.versions.traverse }} These are **runtime** benchmarks. ## Bundle size (tree-shaken, brotli) The default `neotraverse` export is **utility-first** (`sideEffects: false`): your bundler drops unused exports. | Scenario | Brotli (approx.) | | ----------------------------------------------- | ------------------------------------ | | One walk terminal (`forEach`, `map`, `find`, …) | **~{{ brotli.walkTerminalMin }} KB** | | Path helpers only (`get` / `has` / `set`) | **~{{ brotli.pathOnlyMin }} KB** | | All functions except deprecated `Traverse` | **~{{ brotli.allFunctionsMax }} KB** | **Range: {{ bundleSizes.summary.rangeLabel }}**, floor ≈ single traversal, ceiling ≈ full toolkit. Source: [`bench/bundle-sizes.json`](https://github.com/PuruVJ/neotraverse/blob/main/packages/neotraverse/bench/bundle-sizes.json) (`pnpm bundle-size` in `packages/neotraverse`). For a third-party **`traverse` vs neotraverse** comparison, see bundle-roast ↗. _(interactive benchmark charts at https://neotraverse.puruvj.dev/benchmarks)_ ## 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`](/guide/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 · {{ safeMem.treeSize.toLocaleString() }}-node tree · Node {{ safeMem.runtime.node }} | Scenario | default `neotraverse` | `neotraverse/safe` | Verdict | | ---------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | **Deep input** (linked tree, levels) | overflows past **~{{ safeMem.stackSafety.v1MaxDepth.toLocaleString() }}** 💥 | **{{ safeMem.stackSafety.safeDepthTested.toLocaleString() }}+**, no overflow | **/safe only** | | **First 5 of a filtered scan** (peak MB) | {{ earlyExit.v1Mb }} MB | **{{ earlyExit.safeMb }} MB** | **/safe {{ earlyExit.ratio }}× less** | | **Materialize the whole tree** (peak MB) | **{{ fullScan.v1Mb }} MB** | {{ fullScan.safeMb }} MB | default wins ({{ (fullScan.safeMb / fullScan.v1Mb).toFixed(1) }}× 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`](/guide/safe) for the full story.