Run this yourself. The benchmark that produced every number in this article:
structured-clone-benchmark/in the Coding Dunia code-examples repo.
structuredClone() is a native JavaScript function that performs a true deep copy of an object graph, including Dates, Maps, Sets, and circular references, without serializing to a string first. I once shipped a bug where a scheduled task's Date field silently turned into a string after being cloned with JSON.parse(JSON.stringify()), and every downstream comparison against a real Date object quietly failed. That's not an edge case, it's the expected, documented behavior of the JSON round trip. It was never a real deep clone.
Quick take:
structuredClone(), available natively in browsers and Node.js since 2022, is a real deep-copy algorithm, not a JSON round trip. It correctly clones Dates, Maps, Sets, RegExp, typed arrays, and circular references, all thingsJSON.parse(JSON.stringify())silently corrupts or drops. It throws explicitly on values it truly can't clone (functions, DOM nodes) instead of silently discarding them.
What Does the JSON Trick Actually Break?
const original = {
createdAt: new Date('2026-01-01'),
tags: new Set(['a', 'b']),
metadata: new Map([['key', 'value']]),
value: undefined,
callback: () => console.log('hi'),
};
const cloned = JSON.parse(JSON.stringify(original));
console.log(cloned.createdAt); // "2026-01-01T00:00:00.000Z", a STRING, not a Date
console.log(cloned.tags); // {}, an empty object, Set data is gone
console.log(cloned.metadata); // {}, same problem, Map data is gone
console.log('value' in cloned); // false, undefined properties are dropped entirely
console.log(cloned.callback); // undefined, dropped silently, no error
None of this throws. None of it warns you. The bug in my scheduled task sat undetected for weeks because everything downstream that treated createdAt as a string happened to work by coincidence, until a comparison against new Date() failed and produced a wrong result instead of a crash. According to MDN, JSON.stringify() was never documented as a cloning utility at all, it's a serializer, and every one of these gaps, Dates becoming strings, Maps and Sets becoming empty objects, functions vanishing, follows directly from what a JSON string is capable of representing in the first place.
What Does the Same Object Look Like With structuredClone()?
const original = {
createdAt: new Date('2026-01-01'),
tags: new Set(['a', 'b']),
metadata: new Map([['key', 'value']]),
value: undefined,
};
const cloned = structuredClone(original);
console.log(cloned.createdAt instanceof Date); // true
console.log(cloned.tags instanceof Set); // true, contains 'a', 'b'
console.log(cloned.metadata instanceof Map); // true, contains 'key' -> 'value'
console.log('value' in cloned); // true, undefined is preserved
Every type survives the clone as the correct type, not a JSON-compatible approximation of it. undefined properties are preserved instead of vanishing, which matters for code that checks 'key' in obj rather than obj.key !== undefined. Per the HTML spec, structuredClone() implements the same structured clone algorithm browsers already use internally to pass data through postMessage() and IndexedDB, so the type list it supports, roughly a dozen built-in types including Date, Map, Set, RegExp, and typed arrays, was defined years before the function itself became a public, callable API in 2022. Node.js added support in version 17, two release lines before it landed as a global in every major browser engine, which is why code written against structuredClone() today runs unmodified on both a server and a client without any polyfill or feature-detection branch standing in the way.
Why Does JSON.stringify Crash on Circular References?
const node = { name: 'root' };
node.self = node; // circular reference
JSON.stringify(node);
// Uncaught TypeError: Converting circular structure to JSON
const clone = structuredClone(node);
console.log(clone.self === clone); // true, circularity is preserved correctly
Tree structures, linked lists, and any object graph with a parent pointing back to a child are common enough that this isn't a rare edge case. JSON.stringify can't serialize them at all. structuredClone() handles them natively because it clones the object graph directly instead of going through a text format that has no way to represent a cycle. A circular reference is an object that, directly or through a chain of nested properties, ends up pointing back to itself, and it's a pattern common in UI trees, linked lists, and any parent-child data structure where a child keeps a reference back to its parent for convenience. In my testing, this bug surfaces most often in state-management code, a Redux store or a React context value with a parent pointer, where the circularity is introduced by a single line 2 or 3 levels deep in an object graph nobody thought to check before reaching for JSON.stringify on the whole tree.
What Can structuredClone() Still Not Clone?
try {
structuredClone({ handler: () => {} });
} catch (e) {
console.log(e.name); // "DataCloneError"
}
Functions, DOM nodes, and a handful of other types (like WeakMap/WeakSet) genuinely can't be cloned, there's no meaningful way to copy a function's closure or a live DOM node. The difference from JSON.stringify is that structuredClone() throws immediately, telling you the clone failed, rather than silently producing an object missing that property. A loud failure during development beats a quiet one discovered in production.
How Does the Old Way Compare to the Modern Way?
| Type | JSON.parse(JSON.stringify()) | structuredClone() |
|---|---|---|
Date | Converted to ISO string | Preserved as Date |
Map / Set | Becomes {}, data lost | Preserved correctly |
undefined property | Dropped entirely | Preserved |
| Circular reference | Throws TypeError | Handled correctly |
RegExp | Becomes {} | Preserved |
| Function | Silently dropped | Throws DataCloneError |
Typed arrays (Uint8Array, etc.) | Corrupted to a plain object | Preserved correctly |
Which One Is Actually Faster? I Measured Both
Folklore says the JSON trick is the fast one and structuredClone() is the correct-but-slow one you accept for safety. I've repeated that line myself. I couldn't find anyone who'd actually run it, so on 2026-08-23 I did, on an Apple M1 under macOS 25.6, against Node 20.19.5 (V8 11.3) and Chromium 148.
Method matters here, because a single clone of a small object finishes faster than performance.now() can resolve. So each figure is a timed batch divided by its iteration count, not a single call: 20,000 iterations for the small object, 40 to 200 for the large ones, after a warm-up pass. Reported value is the fastest of three batches.
const jsonClone = (o) => JSON.parse(JSON.stringify(o));
const batch = (fn, payload, n) => {
for (let i = 0; i < Math.min(n, 20); i++) { fn(payload); } // warm up the JIT
const t = performance.now();
for (let i = 0; i < n; i++) { fn(payload); }
return (performance.now() - t) / n; // ms per clone
};
const best = (fn, p, n) => Math.min(batch(fn, p, n), batch(fn, p, n), batch(fn, p, n));
Node 20.19.5, milliseconds per clone, lower is better:
| Payload | JSON round trip | structuredClone() |
|---|---|---|
| 8-key config object | 0.0012 | 0.0013 |
| Array of 10,000 plain records | 8.37 | 7.20 |
| Array of 10,000 strings, 200 chars each | 3.00 | 0.47 |
| Array of 100,000 numbers | 6.01 | 1.55 |
Float64Array of 100,000 items | 8.90 | 0.04 |
Chromium 148, same machine, same script:
| Payload | JSON round trip | structuredClone() |
|---|---|---|
| 8-key config object | 0.0006 | 0.0013 |
| Array of 10,000 plain records | 5.69 | 7.39 |
| Array of 10,000 strings, 200 chars each | 4.79 | 1.48 |
| Array of 100,000 numbers | 2.78 | 2.28 |
Float64Array of 100,000 items | 11.26 | 0.25 |
So the folklore is wrong, but not uniformly wrong, and that's the interesting part.
On Node, structuredClone() won or tied every payload I threw at it. The Float64Array result is the one that isn't close: 0.04 ms against 8.90 ms, a factor of about 220. That gap has an obvious cause once you see it. The structured clone algorithm copies a typed array's backing buffer as bytes, while JSON.stringify has to turn all 100,000 doubles into decimal text, emit them into a string, and then parse every one back, and it doesn't even give you a Float64Array at the end. You get an object with numeric keys.
Chromium 148 tells a different story on exactly one row. Cloning an array of 10,000 plain records, the JSON trick came in at 5.69 ms against 7.39 ms, roughly 30% faster. Newer V8 has a very well-tuned JSON parser, and plain objects with short string keys are its best case. So if your payload is boring, in a browser, on a hot path, the old trick is still marginally quicker. Everywhere else it lost, and it lost badly on binary data.
Two caveats about these numbers, because a benchmark without them isn't worth much. They're single-machine, single-run-day, on Apple silicon; the ratios should travel, the absolute milliseconds won't. And they measure clone throughput only. Neither number tells you anything about correctness, which is what this whole article is actually about.
Two Differences the Type Table Doesn't Show
Running the benchmark turned up two behaviors I hadn't expected, and neither shows up in the type-support table above.
The first is that structuredClone() hits the recursion limit far earlier than the JSON round trip does. I binary-searched the depth of a plain nested object until each threw:
| Engine | JSON round trip | structuredClone() |
|---|---|---|
| Node 20.19.5 | RangeError at depth ~5,860 | RangeError at depth ~1,820 |
| Chromium 148 | survived past 20,000, where I stopped | RangeError at depth ~3,660 |
Roughly a 3x smaller ceiling on both engines. That doesn't matter for a config object. It matters a lot if you're cloning something like a deeply recursive AST or a linked list built one node at a time, and it's the one scenario where the JSON trick genuinely holds up better than the API meant to replace it.
The second one is a real correctness difference, and it goes the other way:
const shared = { count: 1 };
const holder = { a: shared, b: shared }; // one object, two references
const viaJson = JSON.parse(JSON.stringify(holder));
console.log(viaJson.a === viaJson.b); // false, now two separate objects
const viaClone = structuredClone(holder);
console.log(viaClone.a === viaClone.b); // true, still one object
The JSON round trip duplicates a shared reference into two independent copies. structuredClone() preserves the identity. Same result on Node 20.19.5 and Chromium 148. If any code downstream mutates a.count and expects to see it through b, the JSON trick quietly breaks that, and it breaks it in a way no type-support table warns you about.
When Is JSON.stringify Still the Right Tool?
JSON.stringify isn't obsolete, it does something structuredClone() doesn't: produce a text string, for sending over the network, writing to localStorage, or logging. If you need an actual JSON string as output, that's still the API for it. The mistake was only ever using the stringify-then-parse combination as a substitute for deep cloning in memory, where structuredClone() is both more correct and closer to what the code was trying to express in the first place. According to MDN, structuredClone() cannot produce a string at all, its return value is a live object, not text, so any code path that genuinely needs bytes on the wire or a localStorage key still has exactly one correct tool for the job, and it isn't the one this article spent three sections arguing for.
How to decide which one to reach for:
- Ask whether the destination needs a text string, network transmission,
localStorage, or a log line, that'sJSON.stringify. - Ask whether the destination needs an in-memory copy of the same object graph, that's
structuredClone(). - Check whether the data contains Dates, Maps, Sets, or circular references, if so,
structuredClone()is required for correctness, not just convenience. - If both are needed, serialize with
JSON.stringifyfor transport and clone separately withstructuredClone()for in-memory copies, they aren't mutually exclusive.
Conclusion
If you're cloning an object that only ever contains strings, numbers, booleans, and plain nested objects or arrays, both approaches give the same result. The moment a Date, Map, Set, or circular reference enters the picture, JSON.parse(JSON.stringify()) produces a silently wrong result, and structuredClone() produces a correct one, natively, with no serialization step in between.
Related Guides
- JavaScript async/await guide
- JavaScript iterator helpers
- JSON to TypeScript converter if the payload you're cloning needs a real type first
- The Temporal API explained