August 20, 2025
· 8 min readDeep Dive: V8's JSON.stringify Optimizations and Deterministic Output
JavaScript's JSON.stringify is fundamental for web developers, and V8 has dramatically improved its performance with new optimizations. This deep dive explores the traditional implementation bottlenecks, V8's new insertion-order-based serialization with fast-path optimizations, and provides practical guidance for leveraging these improvements at scale.

JavaScript's JSON.stringify is fundamental for web developers – from APIs to configuration – yet its performance and output order have subtle implications. Recently, the V8 engine (in Chrome and Node.js) has reworked JSON.stringify to be dramatically faster (often >2× faster on benchmarks). Alongside this speedup, there's growing interest in the determinism of the JSON output.
In this deep dive, we explore how JSON.stringify worked before (its key-order rules and bottlenecks), detail V8's new insertion-order-based serialization and fast-path optimizations, and show real performance gains. We'll illustrate code examples, discuss where deterministic output matters, compare to third-party serializers, and provide practical advice for using JSON.stringify at scale.
Traditional JSON.stringify in V8
By ECMAScript specification, JSON.stringify(obj) iterates object keys in a defined order: all integer-like keys first (in ascending numeric order), then the remaining string keys in insertion order.
let o = {};
o[2] = 2;
o.a = 3;
o.b = 4;
o["1"] = 1;
console.log(JSON.stringify(o));
// {"1":1,"2":2,"a":3,"b":4}
let obj1 = {a: true, b: true};
let obj2 = {b: true, a: true};
console.log(JSON.stringify(obj1), JSON.stringify(obj2));
// {"a":true,"b":true} vs {"b":true,"a":true}The first example sorts keys ("1","2") numerically before ("a","b"), per spec. The second shows two objects with the same keys in different insertion order yielding different strings; this is expected since insertion order of string keys is preserved.
Key takeaway: For a given object, the output is deterministic if the property insertion order is fixed; however, two separately constructed but identical objects may stringify differently if their keys were added in a different sequence.
Under the hood, V8's old JSON.stringify used a straightforward recursive algorithm. It would traverse the value graph, recurse into objects and arrays, and build a single growing buffer for the result. Each time the buffer needed to grow, V8 would reallocate a larger C++ heap buffer and copy the existing characters – a costly step for large JSON. Also, every character in a string was scanned one by one to check if it needed escaping, and large strings or big numeric arrays would be relatively slow to serialize.
The New Side-Effect-Free Fast Path
To address these challenges, V8 13.8 (Chrome 138) introduced a fast path for JSON.stringify that applies when there are no custom side effects or formatting requests.
Fast-path conditions
The call must have:
- No replacer or space parameter – adding either immediately forces the generic serializer
- Ordinary plain-data objects or arrays with no overridden
.toJSON() - No indexed (numeric-like) properties (e.g. treating objects as arrays by adding keys "0","1",...)
Essentially, the fast path assumes "data-only" objects and arrays.
Under these conditions, V8 switches to a new iterative algorithm (instead of recursive) and bypasses many checks. This has two big effects:
- No more stack overhead (so extremely deep nesting is now safe)
- Fewer per-property/per-value tests
For example, the first time an object is serialized, V8 records its hidden class information. If subsequent objects have the same hidden class (common when you serialize an array of similar objects), V8 can use an "express lane" and copy the keys without re-checking them.
In practice: For an array of 10,000 user objects with the same shape, after the first one, V8 reuses that shape info on the rest.
If any of the fast-path conditions fail, V8 "falls back" to the slow, general-purpose path – ensuring correctness at the cost of performance.
Enjoyed this post? Follow on LinkedIn for more engineering insights.
String and Number Optimizations
Even on the fast path, V8 applied further micro-optimizations:
One-byte vs two-byte strings
V8 recognizes that many JSON strings are pure ASCII. It compiles two specialized stringifiers – one for 1-byte characters and one for 2-byte (UTF-16) strings. If a string has no high code-point characters, V8 uses the faster 1-byte routine; if any non-ASCII appears, it switches seamlessly to the 2-byte routine.
SIMD/SWAR for escaping
Scanning a string for escapable chars (", \n, etc.) is a hotspot. The new implementation does this in bulk: for long strings it employs hardware SIMD instructions to check many bytes at once, and for shorter strings it uses SWAR (SIMD Within A Register) bit-twiddling. Most strings (with few or no escapes) can be copied in larger chunks.
Dragonbox for numbers
Converting numbers (floats) to decimal strings used to rely on V8's Grisu3 algorithm with occasional slow fallback cases. V8 has replaced this with the new Dragonbox algorithm. Dragonbox is faster and handles all ranges without fallback, so numeric serialization is smoother.
Segmented Output Buffer
A final improvement was in how V8 builds the output string:
- Old approach: One contiguous buffer growing on demand. Every time it filled up, V8 allocated a new, larger buffer and copied the whole string – an O(n) copy for each resize.
- New approach: A segmented buffer. V8 writes into fixed-size chunks. When one chunk is full, it allocates the next. Only at the very end are all chunks concatenated into one string.
This completely eliminates the repeated copying overhead on large JSON outputs.
Performance Benchmarks
The net effect of all these changes is striking. V8 reports >2× speedups on JSON-heavy benchmarks. For example, the JetStream2 "json-stringify-inspector" test shows over twice the throughput in Chrome 138 / V8 13.8 vs older versions.
Third-party serializers sometimes outperform built-in JSON on small data. For instance, fast-json-stringify (which uses a precompiled schema) can be faster on tiny objects, but native JSON wins on larger arrays:
| Method | Small object (ops/sec) | Large array (ops/sec) |
|---|---|---|
| JSON.stringify (V8) | 3,153,043 | 248 |
| fast-json-stringify | 6,866,434 | ~100 (est.) |
With V8's new optimizations, the built-in JSON.stringify is competitive across many cases.
Real-World Use Cases for Determinism
Deterministic output (same string for the same data) is critical in several contexts:
Caching and memoization
Developers often use JSON.stringify(obj) as a cache key. If two logically identical objects produce different JSON (due to key order), the cache will miss. Ensuring consistent order means the same object graph always hits the same cache entry.
Hashing and signatures
When hashing or signing JSON data (for integrity or crypto), you need a canonical string. RFC 8785 ("JSON Canonicalization Scheme") explicitly defines sorting object properties deterministically so that digital signatures or content hashes match.
Reproducible builds
Tools that emit JSON (lock files, manifests, asset lists) want bit-for-bit reproducibility. A stable key order means two builds with the same inputs yield identical JSON.
Logging and diffs
In distributed systems or testing, comparing JSON payloads assumes a stable order. Deterministic serialization reduces spurious differences.
Comparison with Third-Party Solutions
For extreme performance, schema-based serializers exist. fast-json-stringify requires you to provide a JSON Schema and generates a tailor-made stringify function. It can be much faster on small objects, but the gain shrinks for large, dynamic data.
For deterministic ordering, libraries like json-stable-stringify or json-stringify-deterministic sort object keys during serialization:
const stringifyDet = require('json-stringify-deterministic');
console.log(stringifyDet({c:8, b:[{z:6,y:5,x:4}, 7], a:3}));
// => {"a":3,"b":[{"x":4,"y":5,"z":6},7],"c":8}These libraries guarantee that two semantically equal objects produce the same JSON text, ideal for hashing or signing. The downside is extra overhead from sorting keys.
Practical Tips and Caveats
Stay in the fast lane
- Avoid replacers or formatting in performance-critical code
- Even a single space argument (
JSON.stringify(obj, null, 2)) will divert to the slow path - Use pretty JSON only for debugging
Plain objects/arrays
- Ensure your data is plain
- Don't rely on custom prototype
.toJSON()if you care about speed - Avoid proxies or getters that might have side effects
Numeric keys
- If you want the fast path, don't treat objects as sparse arrays
- Use an actual Array for index-based data, not an object with keys "0","1",...
Versions matter
These optimizations landed in V8 v13.8. To benefit, use:
- Chrome 138+
- Node.js 22+
- Electron with Chromium 138+
Character encodings
If your JSON data includes lots of Unicode (non-ASCII) in values, V8 will take the 2-byte string path, which is slightly more work. Pure ASCII strings are cheaper.
Large outputs
If you need gigabytes of JSON, the segmented buffer helps performance, but the final string is still built in memory. Consider streaming approaches if you can't hold the whole string at once.
Internal Flow Diagram
This diagram highlights the bifurcation between the fast path and general path. Only when conditions are right (no replacer/space, plain data) do we go into the optimized, iterative fast path. That path further splits: if the object's hidden class is already marked (same shape seen earlier), we jump to the express lane and skip most work.
Conclusion
V8's JSON.stringify optimizations represent a significant step forward in JavaScript performance. The combination of fast-path detection, iterative algorithms, SIMD string processing, improved number conversion, and segmented buffering delivers substantial real-world performance gains.
For most developers, simply upgrading to a modern V8 version (Chrome 138+, Node.js 22+) will automatically provide these benefits with no code changes required. The key is understanding when the fast path applies and structuring your data accordingly.
While third-party serializers still have their place for specialized use cases, V8's improvements make the built-in JSON.stringify competitive for the vast majority of applications. Focus on writing clean, predictable code with plain objects and arrays, and let V8's optimizations handle the performance heavy lifting.
FAQ
Is JSON.stringify guaranteed to produce the same string every time?
It's deterministic given the same object structure and insertion order. V8 follows ES spec: integer keys sorted ascending, then other keys in insertion order. So if two objects are semantically equal but were built with different key sequences, their JSON text may differ. For full canonical output across different runs, you'd use a stable-sort serializer.
When will the fast path NOT be used?
Any of these will revert to the slow path: using a replacer function, pretty-print spacing, custom toJSON(), Proxy or exotic prototypes, or adding numeric/string-indexed props. Basically anything that could cause side effects or requires runtime logic will disable the fast path.
How much faster is the new implementation?
Benchmarks show over 2× speedup with V8 13.8 compared to older releases. In real terms, serializing arrays or objects is significantly quicker, and the engine can handle deeper nesting without error. In microbenchmarks, for 'plain' objects you'll often see a 2–4× reduction in time.
Should I switch to fast-json-stringify or another library?
It depends. If you have a fixed data schema and need absolute maximum speed on small payloads, schema-based serializers can beat the native routine. But these add complexity. With the new V8, built-in JSON.stringify is very fast for most use-cases and has no setup cost. Only opt for third-party serializers if you've profiled your app and identified JSON.stringify as the bottleneck.
How can I get deterministic ordering from JSON.stringify?
The simplest way is to ensure you add object keys in the same order every time. If that's not possible, use a stable stringifier library (e.g. json-stringify-deterministic). These will sort keys so that logically equal objects always yield the same JSON string. This is essential for hashing or signing JSON payloads.