Skip to content
Codeloom
JavaScript

JavaScript structuredClone: The Modern Deep Copy Solution

Learn how to use JavaScript structuredClone for deep copying objects, when it beats JSON.parse(JSON.stringify()), and what types it supports and cannot handle.

·7 min read · By Codeloom
Beginner 10 min read

What you'll learn

  • How structuredClone creates deep copies of JavaScript objects
  • Which types structuredClone supports that JSON methods cannot
  • When structuredClone is the wrong tool and what to use instead
  • Performance comparison with alternative deep copy approaches

Prerequisites

  • Basic JavaScript objects and arrays

For years, JavaScript developers have used JSON.parse(JSON.stringify(obj)) as a hacky deep copy mechanism, losing Dates, undefined values, Maps, Sets, and circular references in the process. Since 2022, structuredClone() is available in all modern browsers and Node.js 17+, providing a proper built-in deep copy that handles most JavaScript types correctly.

The Problem with Shallow Copies

Before understanding why structuredClone matters, you need to understand what a shallow copy actually does:

const original = {
  name: "Alice",
  scores: [95, 87, 92],
  address: {
    city: "Portland",
    state: "OR",
  },
};

const shallow = { ...original };

shallow.name = "Bob";
console.log(original.name); // "Alice" - primitives are copied

shallow.scores.push(100);
console.log(original.scores); // [95, 87, 92, 100] - nested arrays are shared

shallow.address.city = "Seattle";
console.log(original.address.city); // "Seattle" - nested objects are shared

The spread operator and Object.assign only copy the top-level properties. Nested objects and arrays are still shared references. Modifying them through the copy modifies the original too.

Using structuredClone

structuredClone creates a completely independent copy, all the way down:

const original = {
  name: "Alice",
  scores: [95, 87, 92],
  address: {
    city: "Portland",
    state: "OR",
  },
};

const deep = structuredClone(original);

deep.scores.push(100);
console.log(original.scores); // [95, 87, 92] - original is untouched

deep.address.city = "Seattle";
console.log(original.address.city); // "Portland" - original is untouched

Types structuredClone Supports

This is where structuredClone shines compared to the JSON trick. It handles types that JSON serialization destroys:

const complex = {
  date: new Date("2026-07-06"),
  pattern: /hello/gi,
  data: new Map([
    ["key1", "value1"],
    ["key2", "value2"],
  ]),
  tags: new Set(["javascript", "web"]),
  binary: new ArrayBuffer(16),
  typedArray: new Uint8Array([1, 2, 3, 4]),
  undef: undefined,
  nan: NaN,
  infinity: Infinity,
  negativeInfinity: -Infinity,
  negativeZero: -0,
};

const cloned = structuredClone(complex);

console.log(cloned.date instanceof Date);   // true
console.log(cloned.pattern instanceof RegExp); // true
console.log(cloned.data instanceof Map);    // true
console.log(cloned.tags instanceof Set);    // true
console.log(cloned.undef);                  // undefined
console.log(Object.is(cloned.nan, NaN));    // true
console.log(Object.is(cloned.negativeZero, -0)); // true

Compare this with the JSON approach:

const jsonCloned = JSON.parse(JSON.stringify(complex));

console.log(jsonCloned.date);     // "2026-07-06T00:00:00.000Z" (string, not Date)
console.log(jsonCloned.pattern);  // {} (empty object)
console.log(jsonCloned.data);     // {} (empty object, Map data lost)
console.log(jsonCloned.tags);     // {} (empty object, Set data lost)
console.log(jsonCloned.undef);    // field is completely missing
console.log(jsonCloned.nan);      // null
console.log(jsonCloned.infinity); // null

Circular References

structuredClone handles circular references that would throw with JSON:

const a = { name: "a" };
const b = { name: "b" };
a.friend = b;
b.friend = a;

// JSON.stringify(a); // throws TypeError: circular reference

const cloned = structuredClone(a);
console.log(cloned.friend.friend === cloned); // true
console.log(cloned !== a);                    // true
console.log(cloned.friend !== b);             // true

The circular structure is preserved in the clone, with all references pointing to cloned objects rather than originals.

Full List of Supported Types

structuredClone supports:

  • Primitives: string, number, boolean, null, undefined, BigInt
  • Date
  • RegExp
  • ArrayBuffer and SharedArrayBuffer
  • Typed arrays: Uint8Array, Int32Array, Float64Array, etc.
  • DataView
  • Map and Set
  • Blob and File
  • ImageData
  • ImageBitmap
  • Plain objects and Array
  • Error objects (with some limitations)

What structuredClone Cannot Clone

Some types will throw a DataCloneError:

// Functions
structuredClone({ fn: () => {} });
// DOMException: Failed to execute 'structuredClone'

// DOM nodes
structuredClone(document.body);
// DOMException: Failed to execute 'structuredClone'

// Symbols
structuredClone({ sym: Symbol("id") });
// DOMException: Failed to execute 'structuredClone'

// WeakMap and WeakSet
structuredClone(new WeakMap());
// DOMException: Failed to execute 'structuredClone'

It also does not clone:

  • Property descriptors (getters, setters, writability, enumerability)
  • Prototype chain (the clone is always a plain object)
  • Non-enumerable properties
  • Proxy objects
class User {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}`;
  }
}

const user = new User("Alice");
const cloned = structuredClone(user);

console.log(cloned instanceof User); // false - prototype is lost
console.log(cloned.greet);           // undefined - methods are lost
console.log(cloned.name);            // "Alice" - data is preserved

Transferable Objects

Like postMessage, structuredClone accepts a transfer list as a second argument:

const buffer = new ArrayBuffer(1024);
console.log(buffer.byteLength); // 1024

const cloned = structuredClone(buffer, { transfer: [buffer] });
console.log(buffer.byteLength);  // 0 - original is detached
console.log(cloned.byteLength);  // 1024 - transferred to clone

Transferring moves the memory instead of copying it, which is faster for large buffers. The original becomes unusable after transfer.

Performance Comparison

How does structuredClone compare to alternatives?

const testData = {
  users: Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `User ${i}`,
    email: `user${i}@example.com`,
    scores: [Math.random(), Math.random(), Math.random()],
    metadata: { created: new Date(), active: true },
  })),
};

function benchmark(label, fn, iterations = 1000) {
  const start = performance.now();
  for (let i = 0; i < iterations; i++) {
    fn();
  }
  const duration = performance.now() - start;
  console.log(`${label}: ${(duration / iterations).toFixed(3)}ms per clone`);
}

benchmark("JSON", () => JSON.parse(JSON.stringify(testData)));
benchmark("structuredClone", () => structuredClone(testData));

In most benchmarks, JSON.parse(JSON.stringify()) is slightly faster for simple data because it is highly optimized. structuredClone is comparable in speed and significantly more correct. For data containing Maps, Sets, Dates, or circular references, structuredClone is the only correct option among the two.

When to Use Each Approach

Use structuredClone when:

  • Your data contains Dates, Maps, Sets, ArrayBuffers, or RegExps
  • Your data might have circular references
  • You need undefined values preserved
  • You want correctness over micro-optimization

Use JSON round-trip when:

  • You need to serialize data for storage or transmission anyway
  • You are targeting environments without structuredClone (very old browsers)
  • Your data is guaranteed to be JSON-safe

Use spread/Object.assign when:

  • You only need a shallow copy
  • Performance is critical and the object is flat
  • You want to merge objects together

Use a library (lodash cloneDeep) when:

  • You need to clone class instances with their prototype chain
  • You need custom handling for specific types
  • You need to clone functions (rare but possible)

Practical Patterns

Immutable State Updates

function updateUser(users, userId, updates) {
  return users.map(user => {
    if (user.id !== userId) return user;
    const cloned = structuredClone(user);
    return Object.assign(cloned, updates);
  });
}

const users = [
  { id: 1, name: "Alice", settings: { theme: "dark", notifications: true } },
  { id: 2, name: "Bob", settings: { theme: "light", notifications: false } },
];

const updated = updateUser(users, 1, { name: "Alicia" });
console.log(users[0].name);   // "Alice" - original unchanged
console.log(updated[0].name); // "Alicia"

Snapshot and Restore

class FormState {
  #data;
  #snapshots = [];

  constructor(initialData) {
    this.#data = structuredClone(initialData);
  }

  saveSnapshot() {
    this.#snapshots.push(structuredClone(this.#data));
  }

  restore() {
    if (this.#snapshots.length > 0) {
      this.#data = this.#snapshots.pop();
    }
  }

  update(path, value) {
    this.saveSnapshot();
    const keys = path.split(".");
    let current = this.#data;
    for (let i = 0; i < keys.length - 1; i++) {
      current = current[keys[i]];
    }
    current[keys[keys.length - 1]] = value;
  }

  getData() {
    return structuredClone(this.#data);
  }
}

Safe Default Objects

const DEFAULT_CONFIG = Object.freeze({
  retries: 3,
  timeout: 5000,
  headers: { "Content-Type": "application/json" },
  backoff: { initial: 100, multiplier: 2, max: 10000 },
});

function createClient(overrides = {}) {
  const config = structuredClone(DEFAULT_CONFIG);
  return deepMerge(config, overrides);
}

Using structuredClone on the frozen default ensures each client gets its own mutable copy.

Wrapping Up

structuredClone is the correct way to deep copy JavaScript objects in modern environments. It handles Dates, Maps, Sets, ArrayBuffers, RegExps, circular references, undefined, NaN, and -0 correctly, which JSON.parse(JSON.stringify()) does not. Its main limitations are that it cannot clone functions, DOM nodes, Symbols, or prototype chains. For most application code where you need a true deep copy of data, structuredClone is the right default choice.