Repository navigation
perf: keep Array.prototype on the primitive-array copy so V8 stays on the JSON.stringify fast path - #903
Merged
Conversation
… the JSON.stringify fast path
Tony133
marked this pull request as ready for review
October 20, 2026 14:45
Member
|
nice! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposal:
Fixes the Node 25+ performance of the primitive-array fast path introduced in #900. Related: #782.
Problem
#900 (shipped in v7.1.2) serializes plain
number,integerandstringarrays above a length cutoff by copying the elements into a fresh array and calling nativeJSON.stringifyon the copy. To guarantee that a user-definedArray.prototype.toJSONis never invoked, the copy's prototype is set tonull.V8's side-effect-free
JSON.stringifyfast path (V8 13.8+, i.e. Node 25+; see https://v8.dev/blog/json-stringify) requires the standardArray.prototype. Any non-standard prototype (null, custom, or anArraysubclass) falls back to the general-purpose serializer at roughly half the speed, so on Node 26 the copy ends up no faster than the per-item loop it replaces:On Node 20/22/24 the fast path does not exist, so the regression is invisible there, which is probably why the cutoffs measured in #900 did not catch it.
Fix
Keep the copy (elements are still read exactly once; the "resume per-item coercion on the first miss" behaviour is untouched) but only detach its prototype when
Array.prototype.toJSONis actually defined, the only case in which the null prototype changes anything. The check is a single monomorphic property load per call, and the lookup walks toObject.prototypeas well.Applied in both places: the generated code for
number/integerarrays inindex.js, andasPlainStringArrayinlib/serializer.js.Benchmark
Median of 5 interleaved rounds, output consumed with
Buffer.byteLength.string[]run withlargeArraySize: 100.Node 26.12.0 (V8 14.6)
Node 24.22.0 (V8 13.6)
Node 22.23.1 (V8 12.4): +0% to +7% on every case. Node 20.20.2 (V8 11.3): within ±4% (noise).
No regression on any runtime; the gain scales with how much of the V8 fast path the runtime has.
Tests
New
test/primitive-array-prototype.test.jscovers, for each ofnumber/integer/string:JSON.stringifywith a pristineArray.prototype;Array.prototype.toJSONis patched;toJSON.