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.
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 DateRegExpArrayBufferandSharedArrayBuffer- Typed arrays:
Uint8Array,Int32Array,Float64Array, etc. DataViewMapandSetBlobandFileImageDataImageBitmap- Plain objects and
Array Errorobjects (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
Proxyobjects
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.
Related articles
- JavaScript JavaScript Streams API: Process Data Incrementally
Master the JavaScript Streams API to process large files, network responses, and data pipelines incrementally without loading everything into memory.
- JavaScript JavaScript Objects: The Most Useful Data Structure
A practical guide to JavaScript objects — creation, access, mutation, iteration, shorthand syntax, destructuring, spread, and how object references actually work.
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript Design Patterns Every Developer Should Know
Learn the most useful JavaScript design patterns with practical examples including Singleton, Observer, Factory, Strategy, and more for cleaner code.