Prevent Copying TypedArrays in Lodash cloneDeepWith
Lodash's standard _.cloneDeep utility recursively clones
every nested structure, including typed arrays and their underlying
memory buffers. When working with massive binary datasets, WebGL
buffers, or audio data, this behavior leads to significant memory spikes
and performance bottlenecks. By utilizing _.cloneDeepWith
and providing a custom interceptor function, you can identify
TypedArray instances and return their original references
directly, bypassing unnecessary memory allocation while still deeply
cloning the rest of the object graph.
The Problem with Default Deep Cloning
By default, _.cloneDeep creates a new instance of any
TypedArray (such as Float32Array,
Uint8Array, or BigInt64Array) and allocates a
brand-new ArrayBuffer with duplicated binary data. When
handling arrays containing millions of elements, this doubling of memory
allocation can quickly degrade performance or trigger out-of-memory
crashes.
Implementing the Customizer Function
The _.cloneDeepWith method accepts a customizer callback
with the signature
customizer(value, key, object, stack).
To prevent cloning large typed arrays:
- Check if the current
valueis aTypedArray. - Optionally check if its
byteLengthexceeds your threshold for what constitutes "huge". - Return the
valuedirectly to reuse the reference. - Return
undefinedfor all other values so Lodash falls back to its standard deep-cloning logic.
Custom Cloning Implementation
The most reliable way to check for a TypedArray in
modern JavaScript is using ArrayBuffer.isView(), excluding
DataView if you wish to clone standard data views.
const _ = require('lodash');
function cloneWithoutHugeTypedArrays(value, byteThreshold = 1024 * 1024) {
return _.cloneDeepWith(value, (val) => {
// Check if the value is a TypedArray (ArrayBuffer view, excluding DataView)
if (ArrayBuffer.isView(val) && !(val instanceof DataView)) {
// Retain the reference if it exceeds the size threshold
if (val.byteLength >= byteThreshold) {
return val;
}
}
// Returning undefined instructs Lodash to use default deep cloning
return undefined;
});
}Usage Example
// Complex state containing normal objects and a large TypedArray
const state = {
id: "dataset-01",
metadata: {
tags: ["production", "sensor-data"],
author: { name: "Engineer" }
},
// 10 million 32-bit floats (~40 MB)
hugeBuffer: new Float32Array(10_000_000)
};
// Perform the clone
const clonedState = cloneWithoutHugeTypedArrays(state);
// Normal properties are deeply cloned (mutating one does not affect the other)
clonedState.metadata.author.name = "Architect";
console.log(state.metadata.author.name); // "Engineer"
// The huge TypedArray shares the underlying memory reference
console.log(clonedState.hugeBuffer === state.hugeBuffer); // trueKey Considerations
- Shared Mutation: Because the reference is
preserved, any mutation made to the elements of the
TypedArrayin the cloned object will directly affect the original object. - Subarray Views: If your object contains multiple
typed arrays pointing to sub-regions of the same underlying
ArrayBuffer, returning the array directly preserves those existing view relationships without fragmenting memory. - Threshold Tuning: If you only want to preserve
memory for truly large arrays, adjust the
byteThresholdcondition. Setting the check purely toArrayBuffer.isView(val)will skip cloning for all typed arrays regardless of size.