Vivian Voss

The Deep Clone You Never Had to Install

javascript web

Stack Patterns ■ Episode 12

You have written this line. You know you have.

const copy = JSON.parse(JSON.stringify(original));

It works. Until it does not. Your Date becomes an ISO string. Your Map becomes an empty object. Your undefined properties vanish without warning. Your RegExp becomes {}. NaN becomes null. Infinity becomes null. BigInt throws a TypeError. Circular references throw a TypeError. And you discover all of this in production, because the test data was flat, simple, and contained none of the types that the real data contained.

For over a decade, JavaScript had no native deep clone. The JSON.parse(JSON.stringify()) hack was the folk remedy: available everywhere, effective enough for simple cases, silently destructive for everything else. Lodash's cloneDeep was the professional fix: 17 KB minified, 22 million weekly downloads, for a function the platform should have provided from the start.

In 2022, the platform did.

The Pattern

const copy = structuredClone(original);

One line. Zero dependencies. Zero kilobytes shipped to the client. It handles Date, RegExp, Map, Set, ArrayBuffer, Blob, File, CryptoKey, Error (including stack and cause), circular references, and over thirty more types. Available in every browser since March 2022, in Node.js since v17, in Deno since 1.14. It has been universally available for over four years.

The History

The structured clone algorithm has existed in the HTML specification since approximately 2010. It was used internally by postMessage() (for sending objects between windows and workers) and IndexedDB (for storing objects). For eleven years, the browser contained a complete, type-aware, cycle-safe deep cloning implementation and simply refused to let you use it. Rather like discovering your kitchen has had a dishwasher since 2010 and you have been washing up by hand.

In 2016, Surma filed WHATWG Issue #793 proposing that the algorithm be exposed as a public function. The resulting pull request took three and a half years to review, iterate, and merge. One does admire the pace.

Why It Works

The JSON hack operates by serialising your object to a text representation and parsing it back. This round-trip through JSON means every type that JSON does not support is either silently dropped, silently transformed, or throws an error.

JSON Hack vs structuredClone Input JSON.parse(JSON.stringify()) structuredClone() DateISO string (loses type)Date (preserved) Map{} (empty object)Map (preserved) Set{} (empty object)Set (preserved) RegExp{} (empty object)RegExp (preserved) undefinedproperty omittedundefined (preserved) NaNnullNaN (preserved) InfinitynullInfinity (preserved) BigIntthrows TypeErrorBigInt (preserved) Circular refthrows TypeErrorhandled correctly Blobthrows TypeErrorBlob (preserved) The JSON hack serialises to text and parses back. Everything JSON does not support is lost. structuredClone walks the object graph directly. No serialisation. No type erasure. No data loss.

structuredClone operates on the object graph directly. It walks the structure, preserves types, tracks references to detect cycles, and produces a genuine copy. No serialisation round-trip. No type erasure. No silent data loss.

What It Does Not Do

structuredClone has honest constraints. It throws a DataCloneError (a DOMException) when it encounters something it cannot clone:

  • Functions: throws immediately. Objects are data; functions are behaviour. The algorithm clones data
  • DOM nodes: throws immediately. Cloning a DOM tree is cloneNode(true), not structuredClone
  • Property descriptors: getters and setters are not duplicated. The result is a plain data copy
  • Prototype chain: discarded. Class instances become plain objects
  • Symbols as keys: silently ignored

These are honest limitations. The JSON hack has many of the same limitations but communicates them by silently returning wrong data. structuredClone throws an exception. One does rather prefer the tool that admits what it cannot do over the tool that pretends it did.

Performance

For small, simple objects with only JSON-safe types, the JSON hack can be faster (approximately 6x in microbenchmarks). This is because JSON.stringify and JSON.parse are highly optimised V8 built-ins, and the structured clone algorithm performs additional work (type checking, cycle tracking) that the JSON path skips.

For complex objects with mixed types, structuredClone performs comparably or better, and does not destroy your data in the process. One benchmark measured 0.25 seconds for structuredClone versus 1.3 seconds for the JSON hack when processing one million objects.

The pragmatic assessment: if your object is simple enough that the JSON hack is faster, it is probably simple enough that a shallow spread ({ ...obj }) suffices. If you need a true deep clone, you need structuredClone.

When to Use

What You Ship for Deep Cloning lodash.cloneDeep17 KB22M dl/week, 10yr old structuredClone0 KBbuilt in, maintained by every browser One is a dependency you chose. The other is a platform feature you already have. Chrome 98, Firefox 94, Safari 15.4, Node.js 17, Deno 1.14. All since March 2022.

Every time you reach for JSON.parse(JSON.stringify()). Every time you consider installing lodash for a single function. Every time you need a true copy of state in a reducer, a cache, a form snapshot, an undo stack, or a worker message.

Lodash cloneDeep: 17 KB, 22 million weekly downloads, ten years without an update. Twenty-two million developers, every week, downloading a package that has not changed since the platform solved the problem. structuredClone: 0 KB, built in, maintained by every browser vendor. One of these is a dependency you chose. The other is a platform feature you already have.

One does note the difference.

JSON.parse(JSON.stringify()) destroys Date, Map, Set, RegExp, undefined, NaN, and throws on circular references. structuredClone handles all of them. Zero KB. Built in since 2022. 30+ types. The browser had the algorithm since 2010 and would not let you use it. Lodash cloneDeep: 17 KB, 22M downloads/week, unchanged for a decade.