structuredClone(): Native Deep Cloning in JavaScript at Last
AI generated
JS
() =>
JavaScript · Web API · Deep Clone · Data Structures
structuredClone()
Native deep cloning at last, no more JSON roundtrip

The JSON roundtrip trick has been the standard for deep cloning in JavaScript for years, and for years it has been losing Date objects, Map, Set, undefined values and circular references. structuredClone() is the native solution that has been available in all modern engines since 2022 and handles all of that correctly.

13 min read structuredClone · Structured Clone Algorithm · Transfer · ArrayBuffer Chrome 98+ · Firefox 94+ · Safari 15.4+ · Node.js 17+

1. The problem with JSON.parse(JSON.stringify())

The trick JSON.parse(JSON.stringify(obj)) is ubiquitous in JavaScript codebases, and it is fundamentally broken for any data type beyond simple strings, numbers and nested objects. Date objects get converted to strings instead of being returned as a Date. Map and Set become empty objects. undefined values disappear from objects and turn into null in arrays. Infinity and NaN become null. Regular expressions become empty objects. And circular references throw a TypeError. These losses happen silently, there is no warning, no error, just a subtly wrong result.

The result in real projects: tests set up with a deep clone trick suddenly no longer match the real data. State management code that takes state snapshots via JSON roundtrip loses date information. Worker communication that serializes objects destroys Map structures. These bugs are hard to debug because the copied object looks superficially correct. structuredClone() solves all of these problems at once and has been available in all modern JavaScript engines without a polyfill since 2022.

2. What structuredClone() is and how it works

structuredClone() is a global web API function that implements the Structured Clone Algorithm, the same algorithm browsers use internally for postMessage(), IndexedDB writes and the History API. The algorithm fully traverses the input object and creates a true, deep copy, every object at every nesting level is created as a new instance. Changes to the clone do not affect the original, and changes to the original do not affect the clone.

The internal implementation of the Structured Clone Algorithm performs an object graph traversal with an identity cache. If the same object is referenced multiple times in the structure, it is cloned only once, and all references in the clone point to that same new copy. This is the mechanism that handles circular references correctly and prevents the algorithm from getting stuck in infinite loops. The identity cache is discarded after the clone operation finishes. structuredClone() is synchronous and blocks the main thread, which can matter for very large objects.


// structuredClone(), correct deep cloning for real-world data
const original = {
  name: 'Mironsoft Project',
  createdAt: new Date('2026-01-15'),           // Date object
  tags: new Set(['typescript', 'magento']),     // Set
  meta: new Map([['version', '1.0'], ['env', 'prod']]), // Map
  pattern: /^\d{4}-\d{2}-\d{2}$/,             // RegExp
  stats: { views: 1024, undefined: undefined }, // undefined value preserved
  buffer: new ArrayBuffer(16),                  // Binary data
};

// JSON.parse(JSON.stringify(original)) would corrupt all of the above
const broken = JSON.parse(JSON.stringify(original));
console.log(broken.createdAt instanceof Date);  // false, it's a string!
console.log(broken.tags);                        // {} (empty object, not a Set)
console.log(broken.pattern);                     // {} (empty object, not RegExp)

// structuredClone() handles all of them correctly
const clone = structuredClone(original);
console.log(clone.createdAt instanceof Date);    // true
console.log(clone.tags instanceof Set);          // true
console.log(clone.meta instanceof Map);          // true
console.log(clone.pattern instanceof RegExp);    // true
console.log('undefined' in clone.stats);         // true

// Verify deep independence: modifying clone does not affect original
clone.name = 'Modified';
clone.tags.add('alpine');
console.log(original.name);         // 'Mironsoft Project' (unchanged)
console.log(original.tags.size);    // 2 (unchanged)

3. Supported types: Date, Map, Set, RegExp and more

The Structured Clone Algorithm supports an explicitly defined list of JavaScript types. Primitive values (Number, String, Boolean, BigInt, null, undefined) are copied. Date objects are cloned as true Date copies with the same timestamp. RegExp objects are cloned with the same pattern and the same flags. Map and Set are cloned fully, including all entries, which are themselves deep cloned in turn. ArrayBuffer, SharedArrayBuffer, typed arrays (Uint8Array, Float64Array etc.) and DataView are copied correctly.

Newer types such as Blob, File, FileList, ImageData, ImageBitmap and CryptoKey are also supported by the Structured Clone Algorithm. structuredClone() can therefore be used for far more scenarios than plain object cloning alone. The support for CryptoKey is especially interesting: cryptographic keys can be cloned and handed over to a Web Worker without needing to import them again. Not supported are functions, DOM elements, errors with stack traces and Proxy objects, for which structuredClone() throws a DataCloneError.

4. Cloning circular references correctly

Circular references are a common pattern in data structures that model parent-child relationships: a child object holds a reference to its parent object, which in turn contains the child object in an array. This object graph cannot be serialized with JSON.stringify(), a TypeError: Converting circular structure to JSON ends the attempt immediately. Many manual recursive clone implementations also fail on circular references because of infinite recursion.

structuredClone() handles circular references correctly with no extra configuration required. The internal identity cache of the Structured Clone Algorithm remembers all objects it has already cloned and, when the same reference is encountered a second time, returns the cached copy instead of cloning it again. The result is a cloned object graph that exhibits the same circularity structure as the original, with the difference that all objects are new instances. This is the correct, expected behavior for a deep clone.


// Circular reference handling and the transfer option
// Circular references: JSON.stringify would throw TypeError
const parent = { name: 'Parent', children: [] };
const child = { name: 'Child', parent };
parent.children.push(child);

// JSON.parse(JSON.stringify(parent)) → TypeError: circular structure
// structuredClone handles it correctly
const clonedParent = structuredClone(parent);
console.log(clonedParent.children[0].parent === clonedParent); // true (structure preserved)
console.log(clonedParent === parent);                            // false (new instance)

// Shared references are preserved correctly
const sharedData = { value: 42 };
const obj = { a: sharedData, b: sharedData };
const cloned = structuredClone(obj);
console.log(cloned.a === cloned.b); // true (same reference within the clone)
console.log(cloned.a === sharedData); // false (new instance, not original)

// Transfer option: move ArrayBuffer ownership (zero-copy)
// Original buffer becomes detached (empty) after transfer
const bigBuffer = new ArrayBuffer(1024 * 1024); // 1 MB
const view = new Uint8Array(bigBuffer);
view[0] = 255;

const transferred = structuredClone(
  { buffer: bigBuffer },
  { transfer: [bigBuffer] } // transfer ownership, do not copy
);

console.log(bigBuffer.byteLength);          // 0 (detached, no longer accessible)
console.log(transferred.buffer.byteLength); // 1048576 (new owner)

5. Transfer: handing over ArrayBuffer ownership

structuredClone() supports an optional second parameter: { transfer: [arrayBuffer1, arrayBuffer2] }. With this option, the specified ArrayBuffer instances are not copied, instead their ownership is transferred. The original becomes detached after the transfer, its byteLength is 0, and any attempt to access it throws a TypeError. The target object receives the same block of memory without the data ever being copied. This is a zero-copy operation.

The transfer mechanism is what postMessage() uses internally for efficient worker communication. If a Web Worker needs to process a 100 MB ArrayBuffer, copying the entire buffer would be expensive. With transfer, ownership moves in microseconds, regardless of buffer size. structuredClone() with the transfer option makes the same pattern available for other scenarios too, without needing a Web Worker or postMessage(), for example for efficiently passing binary data between modules where immutability of the original buffer is desired.

6. What structuredClone() cannot do: functions and prototypes

The most important difference between structuredClone() and libraries such as Lodash cloneDeep() concerns prototypes and class instances. structuredClone() only clones the data of an object, not its prototype. If you clone a class instance with its own methods, the clone contains the same data properties, but it is a plain object copy, it is not an instance of the original class, and its methods are not available. clone instanceof MyClass returns false.

Functions cannot be cloned in general, neither with the Structured Clone Algorithm nor in any meaningful way with other methods. structuredClone() throws a DataCloneError for objects with function properties. This is the main reason why cloneDeep() from Lodash still makes sense in some scenarios: Lodash copies function references and preserves prototypes, while structuredClone() deliberately does not. The choice between the two depends on whether you want to clone data (structuredClone()) or need to copy class instances with methods (Lodash or a manual approach).

7. Performance: structuredClone vs. JSON vs. Lodash

Performance benchmarks for deep cloning depend heavily on the object structure. For flat plain objects with simple primitive values, the JSON roundtrip is often faster than structuredClone(), because JSON.parse/stringify is heavily optimized. But as soon as the object gets deeper or contains Maps, Sets or ArrayBuffers, the picture flips. The JSON roundtrip also has to go through string serialization and deserialization, which is more memory intensive for large objects than the direct object traversal structuredClone() performs.

Lodash cloneDeep() is a complete JavaScript implementation, which is why it is slower than the native implementations. Modern JavaScript engines implement structuredClone() natively in C++ with direct access to internal object representations, which is considerably faster than a JavaScript implementation of the same traversal. For most real-world use cases, state snapshots, worker communication, test setup, the performance difference between structuredClone() and JSON is irrelevant anyway. The decisive factor is correctness, and there structuredClone() is clearly superior.


// Practical patterns for structuredClone() in real applications

// Pattern 1: Immutable state snapshots (Redux / Zustand style)
function createStateManager(initialState) {
  let state = structuredClone(initialState);
  const listeners = new Set();

  return {
    getState: () => structuredClone(state), // return a deep copy, not the reference
    setState(updater) {
      const nextState = updater(structuredClone(state));
      state = nextState;
      listeners.forEach((fn) => fn(structuredClone(state)));
    },
    subscribe(fn) {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
  };
}

// Pattern 2: Test fixture isolation, each test gets its own deep copy
const FIXTURE_USER = {
  id: 1,
  name: 'Test User',
  roles: new Set(['editor', 'viewer']),
  createdAt: new Date('2026-01-01'),
  meta: new Map([['plan', 'pro']]),
};

function getTestUser() {
  return structuredClone(FIXTURE_USER); // isolated copy for each test
}

// Pattern 3: Undo/Redo history with deep snapshots
class UndoHistory {
  constructor(initial) {
    this.stack = [structuredClone(initial)];
    this.index = 0;
  }
  push(state) {
    this.stack.splice(this.index + 1); // discard redo states
    this.stack.push(structuredClone(state));
    this.index++;
  }
  undo() { return this.index > 0 ? structuredClone(this.stack[--this.index]) : null; }
  redo() { return this.index < this.stack.length - 1 ? structuredClone(this.stack[++this.index]) : null; }
}

8. Practical applications: state management, workers, tests

The three most common use cases for structuredClone() in production JavaScript applications are state management, worker communication and test fixtures. In state management, deep copying state before a mutation is the foundation for immutability: instead of modifying state directly, you clone it, modify the clone and replace the old state with the new one. This makes it possible to compare snapshots, implement undo/redo and enable time-travel debugging. With structuredClone() this works even when the state contains Maps, Sets or Dates.

For Web Workers, structuredClone() with transfer is the standard method for efficient binary data handover. Instead of copying an ArrayBuffer, you transfer ownership to the worker, which can read the data immediately, while the main thread side no longer has access. When the result comes back, the worker transfers the buffer back. For test fixtures, structuredClone() is the cleanest way to isolate a shared fixture object: each test gets an independent deep copy, so no test-order dependencies can arise.

9. Deep clone methods compared directly

Choosing the right deep clone method depends on the data types contained in the object to be cloned. The table below shows which method meets which requirements.

Method Date / RegExp Map / Set Circular Functions
structuredClone() Correct Correct Correct Error
JSON.parse/stringify Date → String → {} TypeError Lost
Lodash cloneDeep() Correct Correct Correct Reference copied
Object spread {...obj} Flat Flat Infinite recursion Reference
Object.assign() Flat Flat Infinite recursion Reference

The recommendation is clear: for plain objects, Dates, Maps, Sets and ArrayBuffers, structuredClone() is the first choice. It is native, correct, free of external dependencies and available in all modern environments. When class instances with prototypes need to be cloned, Lodash cloneDeep() is the alternative, though it's worth keeping in mind that class instances rarely need to be deep cloned, since their state usually lives in serializable data. For shallow copies, when you only want to clone the top level of an object without nesting, the spread operator or Object.assign() is sufficient and faster.

Mironsoft

JavaScript code reviews, modernization and robust frontend architectures

Time to clean fragile patterns out of your JavaScript codebase?

We identify fragile JSON roundtrip patterns, error-prone deep clone implementations and outdated dependencies in JavaScript codebases and replace them with native, reliable APIs.

Code audit

Finding JSON.parse/stringify patterns, evaluating Lodash dependencies and building a structuredClone migration plan

State management

Immutable state patterns with structuredClone() for undo/redo, snapshots and time-travel debugging

Worker optimization

Zero-copy ArrayBuffer transfer for Web Workers, binary processing without memory overhead

10. Summary

structuredClone() is the native solution for deep copying in JavaScript, available since 2022 in all modern browsers, Node.js 17+ and Deno. It correctly implements the Structured Clone Algorithm: Date, Map, Set, RegExp, ArrayBuffer, typed arrays, circular references and shared references are all handled correctly. The JSON roundtrip trick silently loses all of these types and should only be used for genuine JSON serialization from now on, not for deep cloning.

The one limitation of structuredClone(): functions and prototypes are not cloned. For class instances with methods, Lodash cloneDeep() is the alternative. For all other scenarios, state management, worker communication, test fixtures, undo/redo, structuredClone() is the most correct, fastest and most dependency-free choice. The transfer option for ArrayBuffer ownership additionally makes a zero-copy handover of binary data possible that was previously only reachable via postMessage().

structuredClone(): the essentials at a glance

Correctly handled types

Date, Map, Set, RegExp, ArrayBuffer, typed arrays, circular references, all cloned correctly. JSON roundtrip loses all of these types.

Limitations

No functions, no prototypes, no DOM elements. DataCloneError for non-clonable types. For class instances with methods: Lodash cloneDeep().

Transfer option

structuredClone(obj, { transfer: [buffer] }) transfers ArrayBuffer ownership without copying. Original becomes detached, zero-copy for binary data.

Availability

Chrome 98+, Firefox 94+, Safari 15.4+, Node.js 17+, Deno. Available globally without an import. No polyfill needed for 2024+ target environments.

11. FAQ: structuredClone()

1structuredClone() vs. JSON roundtrip?
JSON loses Date, Map, Set, undefined and throws on circular references. structuredClone() handles all of them correctly. Use JSON only for genuine serialization, never for deep cloning.
2Cloning class instances with methods?
Not with structuredClone(), prototypes are not cloned. clone instanceof MyClass returns false. For class instances with methods: Lodash cloneDeep().
3What happens to functions?
DataCloneError. Functions reference closures and scopes, not serializable. Objects with function properties cannot be cloned.
4What is the transfer option?
{ transfer: [arrayBuffer] } transfers ownership of the ArrayBuffer without copying. Original becomes detached (byteLength = 0). Zero-copy for efficient binary data handover.
5undefined values correct?
Yes. undefined in object properties is preserved. JSON deletes undefined properties from objects. In arrays, JSON converts undefined to null, structuredClone() preserves that correctly too.
6Node.js availability?
Global since Node.js 17.0.0 without an import. Older Node.js: v8.deserialize(v8.serialize(obj)) or @ungap/structured-clone as a polyfill.
7Shared references?
Handled correctly: the same object is cloned only once, all references in the clone point to that same new copy. The reference topology of the original is preserved.
8Browser support?
Chrome 98+, Firefox 94+, Safari 15.4+, Edge 98+. For older browsers: @ungap/structured-clone as an npm polyfill. Globally available without an import in all supported environments.
9Synchronous or asynchronous?
Synchronous, blocks the main thread. Measurable for very large objects (many MB). For such cases, offload the clone operation to a Web Worker.
10When still use Lodash cloneDeep()?
When class instances with prototypes and methods need to be cloned. For all other scenarios, structuredClone() is preferable, native, faster, no external dependency.