Skip to main content

structuredClone vs JSON.parse(JSON.stringify()) for Deep Copying in Node.js

Updated 5 min readMarkdown
structuredClone vs JSON.parse(JSON.stringify()) for Deep Copying in Node.js

I copied a config object the way everyone does it. JSON.parse(JSON.stringify(obj)), one line, done. The object had a Date in it and a Map. The copy came back, the code ran, and half an hour later something downstream called .get() on what used to be a Map and died. The Map was now {}. The Date was now a string. Nothing had thrown. That is the whole problem with the JSON trick: it fails silently, in the copy, and the error shows up somewhere else entirely.

So I stopped guessing and measured both approaches. Same machine, Node v24.22.1, same object shape at three sizes: 10 items (653 bytes as JSON), 1,000 items (61,380 bytes), 50,000 items (3,499,212 bytes). Each object had a nested Date, a Map, and a few levels of nesting, because a benchmark on plain {a: 1} tells you nothing about real data. I ran each copy 200 times at the small size, 100 at medium, 20 at large, and took the median.

Object size JSON round-trip structuredClone Winner
653 B (10 items) 0.012 ms 0.017 ms JSON, by a hair
61,380 B (1,000 items) 0.792 ms 0.956 ms JSON, ~17% faster
3,499,212 B (50,000 items) 48.162 ms 56.441 ms JSON, ~15% faster

JSON wins on speed at every size, and the gap stays roughly proportional. If your data is plain JSON-shaped stuff — strings, numbers, booleans, arrays, plain objects, null — the old trick is still the faster copy. That is a real result, and it is also the least interesting part of the test.

The interesting part is what each method does to types that are not JSON types. I ran both over one type at a time:

  • Date: JSON turns it into an ISO string. instanceof Date comes back false. structuredClone hands you a real Date back.
  • Map: JSON gives you {}. Empty object, entries gone, no error. structuredClone gives you a working Map, .get("k") returns 1.
  • RegExp: JSON gives you {}. structuredClone gives you a real RegExp.
  • undefined as a value: JSON drops the key entirely. structuredClone keeps the key, value undefined.
  • Function: JSON drops the key and carries on. structuredClone throws DataCloneError and copies nothing at all.
  • BigInt: JSON.stringify throws TypeError: Do not know how to serialize a BigInt. structuredClone copies 10n fine.
  • Circular reference: JSON.stringify throws TypeError. structuredClone handles it, and the copy's self points at the copy itself (copy.self === copy is true).

Read that list again, because it splits into two very different kinds of failure. JSON mostly fails quiet: it drops keys, empties Maps, flattens Dates to strings, and hands you something that looks fine until you use it. structuredClone mostly fails loud: a function in the object means the whole copy throws. Loud is better. A throw at the copy site tells you where the problem is. A string where a Date should be tells you nothing until a .getTime() call three modules away blows up on a Tuesday.

There is one more habit worth dropping. A lot of codebases copy with JSON because "structuredClone is slower" and then never check the assumption. On my numbers, the gap is about 15% on a 3.5 MB object, and under a millisecond on anything smaller. If you are copying objects that big in a hot loop, the copy method is not your problem — the fact that you are copying 3.5 MB objects in a hot loop is. For ordinary sizes, pick by correctness, not by 0.005 ms.

My rule now, and it has held up since the Map incident:

  • Data came from JSON, or is going back to JSON? Use the JSON round-trip. It is faster, and nothing can get mangled, because there was nothing non-JSON in it to begin with.
  • Data has Dates, Maps, Sets, RegExps, typed arrays, or circular references? Use structuredClone. The 15% is cheap insurance against a silent type change.
  • Data might contain functions? Neither method copies them. JSON drops them quietly, structuredClone refuses. If you need functions in the copy, you need a custom clone, not either of these.

One last detail for the skeptics: the benchmark object itself contained a Date and a Map, and the timings above include whatever each method did with them. JSON was still faster while doing less work — it serializes a Date to a string and a Map to {}, which is less work than cloning them properly. The speed gap is partly a correctness gap. You are not comparing two ways of doing the same thing. You are comparing a fast lossy copy with a slower faithful one, and the right pick depends on whether your data can afford to lose anything.

P.S. Everything I write stays free — that part doesn't change. But if this one was worth something to you, you can buy me a coffee ☕ (one-time or monthly, both work). It covers the API credits and hosting behind these experiments — and honestly, it's also the nudge that gets the next one started on the evenings I'd otherwise skip it. — Salam

Buy me a coffee

No comments yet. Be the first to comment.

More from this blog