Skip to content

structuredClone vs JSON.stringify vs Spread: Deep Copy Guide

Compare structuredClone, the JSON trick and spread for deep copying in JavaScript. Tested results: what breaks, what’s faster, and which one to use.

Dasun Kalupahana 3 min read 66 views 0 comments
On this page
  1. Why a plain copy is not enough
  2. The JSON trick, and what it quietly destroys
  3. structuredClone(): the built-in deep copy
  4. What structuredClone() cannot do
  5. Is it faster? Not in my tests
  6. Which one should you use?
  7. A bonus: moving large data without copying
  8. References

Sooner or later every JavaScript developer copies an object, changes the copy, and finds the original changed too. The usual fixes are the spread operator, the old JSON.parse(JSON.stringify(obj)) trick, and the newer structuredClone(). They look interchangeable. They are not, and the differences show up as bugs only when your data contains dates, sets, undefined, or a circular reference. I ran all three on Node 22 and in Chromium, and checked the behavior against MDN, so you can see what each one really does.

Why a plain copy is not enough

Spread ({...obj}) and Object.assign make a shallow copy: top-level values are copied, but nested objects are shared. I copied an object with spread and pushed to a nested array on the copy:

const original = { nested: { list: [1, 2] } };
const copy = { ...original };

copy.nested.list.push(3);

console.log(original.nested.list);   // [ 1, 2, 3 ]  - the original changed

If your object has nested data and you plan to change it, you need a deep copy.

The JSON trick, and what it quietly destroys

JSON.parse(JSON.stringify(obj)) works by turning your data into text and back. That means anything JSON cannot represent is changed or lost. These are real outputs from Node 22:

ValueJSON round-tripstructuredClone()
DateBecomes an ISO stringStays a real Date
undefined propertyKey disappearsKept
NaN, InfinityBecome nullKept
Set, MapBecome {}Cloned correctly
RegExpBecomes {}Cloned, flags kept
BigIntThrows a TypeErrorCloned
Circular referenceThrows a TypeErrorCloned, cycle preserved
Function propertySilently droppedThrows DataCloneError
-0Becomes 0Kept

The dangerous rows are the silent ones. A Date turning into a string, or a Set turning into {}, does not throw anything. Your code just breaks later, somewhere else.

structuredClone(): the built-in deep copy

const order = {
  id: 7,
  createdAt: new Date('2026-01-01T00:00:00Z'),
  tags: new Set(['paid', 'gift']),
  note: undefined,
};

const copy = structuredClone(order);

copy.createdAt instanceof Date;   // true
copy.tags instanceof Set;         // true
'note' in copy;                   // true

It handles circular references too, and keeps shared references shared. When I cloned { a: shared, b: shared }, the clone's a and b were the same object, but a different object from the original shared. MDN lists it as widely available in browsers since March 2022, and I confirmed it exists in Node 22 and in Chromium 141.

What structuredClone() cannot do

  • Functions throw. structuredClone({ fn() {} }) raised a DataCloneError (a DOMException). The same happened for a Promise and a WeakMap. A Symbol value throws too, though a Symbol used as a key is silently skipped. In a browser, DOM nodes throw as well, which I confirmed with document.body.
  • Class instances lose their class. I cloned new User('ada') and got a plain object with name only. It was not an instanceof User, and the greet() method was gone. MDN says the prototype chain is not walked or duplicated.
  • Getters become plain values. An object with get x() { return 5 } was cloned as { x: 5 }, a normal writable property. Property descriptors are not copied.
  • Non-enumerable properties are dropped.

If you need to copy class instances, write a clone() method on the class, or rebuild the instance from the cloned data with new User(...).

Is it faster? Not in my tests

You may read that structuredClone is the fast option. On plain JSON-like data, it was not. Median of 5 runs, Node 22 on a small container:

DatastructuredCloneJSON round-trip
Small object (about 10 values)4.0 µs1.9 µs
5,000 user objects14.6 ms10.2 ms

Chromium 141 showed the same ordering (4.9 vs 1.6 µs, and 16.7 vs 10.9 ms). So the JSON trick was faster on plain data in both. The numbers are from one machine and one data shape, so use them for direction, not as exact figures. The point: choose structuredClone for correctness, not speed. If you are cloning in a hot loop, ask whether you need a deep copy at all.

Which one should you use?

SituationUse
Flat object, or you only change top-level valuesSpread: { ...obj }
Nested data, may contain dates, Sets, Maps, undefined or cyclesstructuredClone()
Plain JSON-safe data and you want the fastest optionJSON round-trip works, if you are sure of the data
Contains functions or class instances you need to keepA custom clone, or a library such as lodash's cloneDeep (not tested here)
Copying state in React or similarUsually avoid deep copies: spread only the path you change

A bonus: moving large data without copying

structuredClone has a second argument for transferring data instead of copying it. I created an 8-byte ArrayBuffer and cloned it with { transfer: [buffer] }. The clone had 8 bytes, and the original dropped to 0 bytes. It is useful for big binary data, but the original becomes unusable.

References

Free tools for this topic

Code snippets and fixes for this topic

Get one practical tip every week

New tutorials, fixes and free cheat sheets from DEV Talk, straight to your inbox. No spam, unsubscribe in one click.

Have a question about this post?

Ask the community: other developers and the author can answer.

Ask the community

Get notified about new articles

One click, no email needed. Turn it off anytime with the bell at the top.

Get new articles on your iPhone

Add DEV Talk to your Home Screen, open it, then tap the bell.

Rate this post
0.0
Share

Enjoying the content?

If this article helped you, you can support the author with a coffee. It keeps new posts coming.

Buy me a coffee (opens in a new tab)

Comments 0

Be kind. Comments with links are reviewed before they appear.

No comments yet

Be the first to share your thoughts.

Get new articles in your inbox

Practical tutorials and deep dives, about once a week. No spam, unsubscribe anytime.

Privacy policy

Add DEV Talk to your Home Screen

  1. 1 Tap the Share button in Safari's toolbar.
  2. 2 Scroll down and tap Add to Home Screen.
  3. 3 Tap Add. Open it from the Home Screen to read like an app and to turn on notifications with the bell.

We use cookies to improve your experience and analyze traffic. By continuing, you agree to our Privacy Policy.