Toggle navigation
MeasureThat.net
Create a benchmark
Tools
Feedback
FAQ
Register
Log In
Run results for:
_.cloneDeep vs structuredClone vs cloneDeep
https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
Go to the benchmark
Embed
Embed Benchmark Result
Run details:
User agent:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Browser:
Chrome 133
Operating system:
Mac OS X 10.15.7
Device Platform:
Desktop
Date tested:
one year ago
Test name
Executions per second
Lodash cloneDeep
3915.8 Ops/sec
Native structuredClone
5390.4 Ops/sec
mt cloneDeep
1003.2 Ops/sec
HTML Preparation code:
<script src='https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js'></script>
Script Preparation code:
const Tag = Object.freeze({ ARGUMENTS: '[object Arguments]', ARRAY: '[object Array]', BOOLEAN: '[object Boolean]', DATE: '[object Date]', ERROR: '[object Error]', MAP: '[object Map]', NUMBER: '[object Number]', OBJECT: '[object Object]', REGEXP: '[object RegExp]', SET: '[object Set]', STRING: '[object String]', SYMBOL: '[object Symbol]', WEAKMAP: '[object WeakMap]', WEAKSET: "[object WeakSet]", ARRAYBUFFER: '[object ArrayBuffer]', DATAVIEW: '[object DataView]', FLOAT32: '[object Float32Array]', FLOAT64: '[object Float64Array]', INT8: '[object Int8Array]', INT16: '[object Int16Array]', INT32: '[object Int32Array]', UINT8: '[object Uint8Array]', UINT8CLAMPED: '[object Uint8ClampedArray]', UINT16: '[object Uint16Array]', UINT32: '[object Uint32Array]', BIGINT64: "[object BigInt64Array]", BIGUINT64: "[object BigUint64Array]" }); function cloneInternalNoRecursion(_value, customizer, log, doThrow) { if (typeof log !== "function") log = console.warn; let result; // Will be used to store cloned values so that we don't loop infinitely on // circular references. const cloneStore = new Map(); // This symbol is used to indicate that the cloned value is the top-level // object that will be returned by the function. const TOP_LEVEL = Symbol("TOP_LEVEL"); // A queue so we can avoid recursion. const queue = [{ value: _value, parentOrAssigner: TOP_LEVEL }]; // We will do a second pass through everything to check Object.isExtensible, // Object.isSealed and Object.isFrozen. We do it last so we don't run into // issues where we append properties on a frozen object, etc const isExtensibleSealFrozen = []; function warn(message, cause) { class CloneDeepWarning extends Error { constructor(message, cause) { super(message, cause); this.name = CloneDeepWarning.name; } } return new CloneDeepWarning(message, cause); } function assign(cloned, parentOrAssigner, prop, metadata) { if (parentOrAssigner === TOP_LEVEL) result = cloned; else if (typeof parentOrAssigner === "function") parentOrAssigner(cloned, prop, metadata); else if (typeof metadata === "object") { const hasAccessor = ["get", "set"].some(key => typeof metadata[key] === "function"); // `cloned` or getAccessor will determine the value delete metadata.value; // defineProperty throws if property with accessors is writeable if (hasAccessor) { delete metadata.writable; log(warn("Cloning value whose property descriptor is a get " + "or set accessor.")); } Object.defineProperty(parentOrAssigner, prop, Object.assign( // defineProperty throws if value and set/get accessor coexist hasAccessor ? {} : { value: cloned }, metadata, )); } else parentOrAssigner[prop] = cloned; return cloned; } function tagOf(value) { return Object.prototype.toString.call(value); } for (let obj = queue.shift(); obj !== undefined; obj = queue.shift()) { // `value` is the value to deeply clone // `parentOrAssigner` is either // - TOP_LEVEL - this value is the top-level object that will be // returned by the function // - object - a parent object this value is nested under // - function - an "assigner" that has the responsiblity of // assigning the cloned value to something // `prop` is used with `parentOrAssigner` if it is an object so that the // cloned object will be assigned to `parentOrAssigner[prop]`. // `metadata` contains the property descriptor(s) for the value. It may // be undefined. const { value, parentOrAssigner, prop, metadata } = obj; // Will contain the cloned object. let cloned; // Check for circular references. const seen = cloneStore.get(value); if (seen !== undefined) { assign(seen, parentOrAssigner, prop, metadata); continue; } // If true, do not not clone the properties of value. let ignoreProps; // If true, do not have `cloned` share the prototype of `value`. let ignoreProto; // Is true if the customizer determines the value of `cloned`. let useCustomizerClone; // Perform user-injected logic if applicable. if (typeof customizer === "function") { let clone, additionalValues, ignore; try { const customResult = customizer(value); if (typeof customResult === "object") { useCustomizerClone = true; // Must wrap destructure in () if not variable declaration ({ clone, additionalValues, ignore, ignoreProps, ignoreProto } = customResult); if (ignore === true) continue; cloned = assign(clone, parentOrAssigner, prop, metadata); if (Array.isArray(additionalValues)) additionalValues.forEach(object => { if (typeof object === "object") { queue.push({ value: object.value, parentOrAssigner: object.assigner }); } }); } } catch(error) { if (doThrow === true) throw error; clone = undefined; useCustomizerClone = false; error.message = "customizer encountered error. Its results " + "will be ignored for the current value, and " + "the algorithm will proceed with default " + "behavior. Error encountered: " + error.message; log(warn(error.message, error.cause)); } } try { // skip the following "else if" branches if (useCustomizerClone === true) {} // If value is primitive, just assign it directly. else if (value === null || !["object", "function"] .includes(typeof value)) { assign(value, parentOrAssigner, prop, metadata); continue; } // We won't clone weakmaps or weaksets. else if ([Tag.WEAKMAP, Tag.WEAKSET].includes(tagOf(value))) throw warn(`Attempted to clone unsupported type${ typeof value.constructor === "function" && typeof value.constructor.name === "string" ? ` ${value.constructor.name}` : "" }.`); // We only copy functions if they are methods. else if (typeof value === "function") { cloned = assign(parentOrAssigner !== TOP_LEVEL ? value : {}, parentOrAssigner, prop, metadata); log(warn(`Attempted to clone function${typeof prop === "string" ? ` with name ${prop}` : "" }. ` + "JavaScript functions cannot be cloned. If this " + "function is a method, then it will be copied "+ "directly.")); if (parentOrAssigner === TOP_LEVEL) continue; } // If value is a Node Buffer, just use Buffer's subarray method. else if (typeof global === "object" && global.Buffer && typeof Buffer === "function" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(value)) cloned = assign(value.subarray(), parentOrAssigner, prop, metadata); else if (Array.isArray(value)) cloned = assign(new Array(value.length), parentOrAssigner, prop, metadata); // Ordinary objects, or the rare `arguments` clone else if ([Tag.OBJECT, Tag.ARGUMENTS].includes(tagOf(value))) cloned = assign(Object.create(Object.getPrototypeOf(value)), parentOrAssigner, prop, metadata); // values that will be called using contructor else { const Value = value.constructor; // Booleans, Number, String or Symbols which used `new` syntax // so JavaScript thinks they are objects // We also handle Date here because it is convenient if ([Tag.BOOLEAN, Tag.DATE].includes(tagOf(value))) cloned = assign(new Value(Number(value)), parentOrAssigner, prop, metadata); else if ([Tag.NUMBER, Tag.STRING].includes(tagOf(value))) cloned = assign(new Value(value), parentOrAssigner, prop, metadata); else if (Tag.SYMBOL === tagOf(value)) { cloned = assign( Object(Symbol.prototype.valueOf.call(value)), parentOrAssigner, prop, metadata); } else if (Tag.REGEXP === tagOf(value)) { const regExp = new Value(value.source, /\w*$/.exec(value)); regExp.lastIndex = value.lastIndex; cloned = assign(regExp, parentOrAssigner, prop, metadata); } else if (Tag.ERROR === tagOf(value)) { const cause = value.cause; cloned = assign(cause === undefined ? new Value(value.message) : new Value(value.message, { cause }), parentOrAssigner, prop, metadata); } else if (Tag.ARRAYBUFFER === tagOf(value)) { // copy data over to clone const arrayBuffer = new Value(value.byteLength); new Uint8Array(arrayBuffer).set(new Uint8Array(value)); cloned = assign(arrayBuffer, parentOrAssigner, prop, metadata); } // TypeArrays else if ([ Tag.DATAVIEW, Tag.FLOAT32, Tag.FLOAT64, Tag.INT8, Tag.INT16, Tag.INT32, Tag.UINT8, Tag.UINT8CLAMPED, Tag.UINT16, Tag.UINT32, Tag.BIGINT64, Tag.BIGUINT64 ].includes(tagOf(value))) { // copy data over to clone const buffer = new value.buffer.constructor( value.buffer.byteLength); new Uint8Array(buffer).set(new Uint8Array(value.buffer)); cloned = assign( new Value(buffer, value.byteOffset, value.length), parentOrAssigner, prop, metadata); } else if (Tag.MAP === tagOf(value)) { const map = new Value; cloned = assign(map, parentOrAssigner, prop, metadata); value.forEach((subValue, key) => { queue.push({ value: subValue, parentOrAssigner: cloned => { isExtensibleSealFrozen.push([subValue, cloned]); map.set(key, cloned) } }); }); } else if (Tag.SET === tagOf(value)) { const set = new Value; cloned = assign(set, parentOrAssigner, prop, metadata); value.forEach(subValue => { queue.push({ value: subValue, parentOrAssigner: cloned => { isExtensibleSealFrozen.push([subValue, cloned]); map.set(key, cloned) } }); }); } else throw warn("Attempted to clone unsupported type."); } } catch(error) { error.message = "Encountered error while attempting to clone " + "specific value. The value will be \"cloned\" " + "into an empty object. Error encountered: " + error.message log(warn(error.message, error.cause)); cloned = assign({}, parentOrAssigner, prop, metadata); // We don't want the prototype if we failed and set the value to an // empty object. ignoreProto = true; } cloneStore.set(value, cloned); isExtensibleSealFrozen.push([value, cloned]); // Ensure clone has prototype of value if (ignoreProto !== true && Object.getPrototypeOf(cloned) !== Object.getPrototypeOf(value)) Object.setPrototypeOf(cloned, Object.getPrototypeOf(value)); if (ignoreProps === true) continue; // Now copy all enumerable and non-enumerable properties. [Object.getOwnPropertyNames(value), Object.getOwnPropertySymbols(value)] .flat() .forEach(key => { queue.push({ value: value[key], parentOrAssigner: cloned, prop: key, metadata: Object.getOwnPropertyDescriptor(value, key) }); }); } // Check extensible, seal, and frozen statuses. isExtensibleSealFrozen.forEach(([value, cloned]) => { if (!Object.isExtensible(value)) Object.preventExtensions(cloned); if (Object.isSealed(value)) Object.seal(cloned); if (Object.isFrozen(value)) Object.freeze(cloned); }); return result; } function cloneDeep(value, options) { if (typeof options === "function") options = { customizer: options }; else if (typeof options !== "object") options = {}; let { customizer, log, logMode, letCustomizerThrow } = options; if (logMode !== "string" || typeof log === "function"); else if (logMode.toLowerCase() === "silent") log = () => { /* no-op */ }; else if (logMode.toLowerCase() === "quiet") log = error => console.warn(error.message); return cloneInternalNoRecursion(value, customizer, log, letCustomizerThrow); } var myObject = {}; let next = myObject; for (let i = 0; i < 1000; i++) { next.b = {}; next = next.b; } let myCopy;
Tests:
Lodash cloneDeep
myCopy = _.cloneDeep(myObject);
Native structuredClone
myCopy = structuredClone(myObject);
mt cloneDeep
myCopy = cloneDeep(myObject);