Skip to content

perf(arrays): inline compact primitives - #77

Merged
BridgeAR merged 7 commits into
mainfrom
BridgeAR/2026-10-13-stringify-primitives
Oct 15, 2026
Merged

BridgeAR merged 7 commits into
mainfrom
BridgeAR/2026-10-13-stringify-primitives

Conversation

@BridgeAR

Copy link
Copy Markdown
Owner

Compact arrays pay recursive dispatch and index conversion for strings, finite numbers, and booleans.
Safe mode retains per-element exception handling.

A four-number array uses 101 ns instead of 133 ns, a 24% CPU reduction on Node 26.3.1 (V8 14.6.202.34-node.20).
Measurements use 50,000 warmup calls and seven alternating trials, with the fastest and slowest trials removed.
Four-object arrays use about 5% more CPU in both confirmation runs.

Drive-by fix: Mark intentional invalid-option assertions for TypeScript.

@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-10-13-stringify-primitives branch from f86ee76 to 39cbfad Compare October 13, 2026 05:51
A four-number array uses 101 ns instead of 133 ns on Node 26.3.1
(V8 14.6.202.34-node.20), a 24% CPU reduction. Measurements use 50,000 warmup
calls and seven alternating trials, with the fastest and slowest removed.
Four-object arrays use about 5% more CPU in both confirmation runs.
Safe mode retains per-element exception handling.

Drive-by fix: Mark intentional invalid-option assertions for TypeScript.
Compact object arrays pay primitive checks before recursive serialization,
which adds overhead to the existing object path.

Four-number arrays use 69.8 ns instead of 75.4 ns on Node 26.3.1
(V8 14.6.202.34-node.20), a 7.4% reduction. Measurements use 100,000 warmup
calls and seven alternating trials, with the fastest and slowest removed.

Safe mode must catch primitive failures per value so an oversized string
does not replace its containing array.
On Node 26.3.1 (V8 14.6.202.34-node.20), a four-number array with
indentation takes 82.8 ns instead of 100.7 ns, an 18% CPU reduction.
Measurements use 100,000 warmup calls and seven alternating trials of
200,000 calls, with the fastest and slowest results removed.

Function replacers already catch failures per value in safe mode.
Reusing that boundary avoids a second wrapper around primitive formatting.
@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-10-13-stringify-primitives branch from 39cbfad to dc389b9 Compare October 13, 2026 06:35
TAP 21 and newer processinfo releases cannot support the Node.js 18 test
matrix. TypeScript 6 retains legacy Node resolution and works with TAP's
current TypeScript loader.
Against main, the comparison fixture increases from 68,305 to 76,684 ops/sec
on Node.js 26.3.1 (V8 14.6.202.34-node.20), with 50,000 warmup calls and seven
two-second trials excluding the fastest and slowest results.

Some deep-object benchmark cases measure up to 3.1% slower. Shared primitive
handling reduces published source size by 5.2% while preventing the implementations
from diverging.
The primitive error matrix encodes a maximum-length string 18 times.
On Node.js 18.20.8 (V8 10.2.154.26-node.39), that takes 22.1 seconds.
Slower CI runners reach the 30-second timeout before the suite completes.

The mode matrix uses a controlled RangeError at the native escaping
boundary. Existing tests still exercise the real string limit. The full
Node.js 18 suite takes 4.1 seconds after a clean install.
Array replacers cannot change a primitive root's output. Skip their
preprocessing, including getter and iterator reads, for these inputs.

A numeric root with an eight-entry array replacer takes 8.4 ns instead of
97.4 ns on Node 26.3.1 (V8 14.6.202.34-node.20). This averages two fresh
processes with seven alternating trials after 50,000 warmup calls,
discarding the fastest and slowest trials.
@BridgeAR
BridgeAR marked this pull request as ready for review October 15, 2026 07:55
@BridgeAR
BridgeAR merged commit bdd585f into main Oct 15, 2026
5 checks passed
@BridgeAR
BridgeAR deleted the BridgeAR/2026-10-13-stringify-primitives branch October 15, 2026 07:59
kitschpatrol pushed a commit to kitschpatrol/safe-stable-stringify that referenced this pull request Nov 2, 2026
* fix: do not throw on a negative number `space` (BridgeAR#65)

`JSON.stringify` treats any numeric `space` below 1 as no indentation, but
`stringify` threw on it:

    JSON.stringify({ a: 1 }, null, -1) // '{"a":1}'
    stringify({ a: 1 }, null, -1)      // RangeError: Invalid count value: -1

The README documents the spacer as behaving "the same as with
JSON.stringify()". `space` was clamped to a maximum of 10 but not a minimum
of 0, so a negative count reached `String.prototype.repeat`, which throws
for counts <= -1. Clamp the lower bound to 0.

* Fix merge conflict in readme (BridgeAR#63)

Looks like this was accidentally introduced in BridgeAR@132e76c

* fix: enforce maximumDepth for nested objects with array replacers (BridgeAR#64)

* feat(circular)!: include ancestor depth in default markers (BridgeAR#70)

Circular references to different ancestors currently produce the same marker, which hides where each cycle closes. The new marker identifies the referenced ancestor's depth, starting at 0 for the root.

BREAKING CHANGE: The default marker changes from `[Circular]` to `[Circular: N]`. Set `circularValue: '[Circular]'` to retain the previous output.

* fix(typed-array): serialize non-finite floating-point values (BridgeAR#73)

* Serialize non-finite typed-array values consistently

Fixes: BridgeAR#68

* perf(typed-array): inline floating-point checks

A formatter closure adds finite checks to integer arrays and an extra call for floating-point values. Inline checks retain direct conversion for integer and BigInt arrays.

---------

Co-authored-by: Ferit <frtcm11@gmail.com>

* docs(circular): clarify JSON escaping for custom markers (BridgeAR#74)

* perf(arrays): defer index string conversion (BridgeAR#75)

Array indexes only need strings when they reach toJSON or function replacer
callbacks. A four-number array uses 86 ns instead of 107 ns on Node 26.3.1
(V8 14.6.202.34-node.20), a 20% CPU reduction. Measurements use 50,000 warmup
calls and seven alternating trials, with the fastest and slowest removed.

Drive-by fix: Mark intentional invalid-option assertions for TypeScript.

* perf(sort): bound insertion sort work (BridgeAR#76)

Disordered objects with up to 200 keys can spend most of their serialization
time shifting keys. A shuffled 200-key object uses 16.6 us instead of 45.6 us
on Node 26.3.1 (V8 14.6.202.34-node.20), a 64% CPU reduction. Measurements
use 50,000 warmup calls and seven alternating trials, with the fastest and
slowest removed. Nearly ordered keys retain the insertion path.

Drive-by fix: Mark intentional invalid-option assertions for TypeScript.

* feat(modules)!: share ESM and CommonJS exports (BridgeAR#72)

The implementation is cached after the first load, so removing its global
export leaves the other wrapper without an export. Retain a version-specific
symbol so ESM imports, CommonJS requires, and independent copies of one
version receive the same function.

BREAKING CHANGE: Require Node.js 18 or later.

* perf: reduce primitive serialization overhead (BridgeAR#77)

Primitive array elements and object properties enter recursive serializers
even though they need no traversal. Share primitive formatting across
serialization modes while preserving toJSON ordering and per-value error
handling in safe mode.

Native string escaping becomes faster at shorter string lengths on
Node.js 22 and later. Select the cutoff once at module load so older
runtimes retain their existing fast path.

---------

Co-authored-by: Spoko Dev <spoko.dev@gmail.com>
Co-authored-by: Andy Trevorah <andy@trevorah.net>
Co-authored-by: Chamath Adithya <achamath1@gmail.com>
Co-authored-by: Ruben Bridgewater <ruben@bridgewater.de>
Co-authored-by: Ferit <frtcm11@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant