Something that I’ve been looking forward to using post Node 16 is structuredClone(). It’s been available in browser for a little while and was introduced in Node version 17.0.0, it’s an answer to the lack of a decent way to clone objects in JavaScript. Making a shallow clone of an object is pretty easy using Object.assign({}, objToClone) however in practice we often want to clone objects containing nested objects and arrays, for that we often resort to JSON.parse(JSON.stringify(objToClone)) this is a bit of an ugly and long winded solution but in most cases it works. Unfortunately there are certain things that JSON can’t handle, for example:

const objToClone = {
  name: 'Obj with a circular reference'
}
objToClone.self = objToClone

const clone = JSON.parse(JSON.stringify(objToClone))

// Output:
// TypeError: Converting circular structure to JSON

Using A Library

Until now the solution has generally been to use a library such as Lodash which has a handy _.cloneDeep() method to help with making copies of complex objects. Pre ES6 I used Lodash extensively in many of my projects but there’s days I rarely need it, the question therefore is can structuredClone replace _.cloneDeep()?

The Performance Of structuredClone()

Unfortunately running Node 18.6.0, the performance of structuredClone() isn’t great. In my very simple test below, cloning a simple object with a circular reference 10,000,000 times, it’s about a third as fast as _.cloneDeep()

const _ = require('lodash')

const objToClone = {
  name: 'Obj with a circular reference'
}
objToClone.self = objToClone

// structuredClone Test

console.time('structuredClone')

for (let i = 0; i < 10000000; i++) {
  structuredClone(objToClone)
}

console.timeEnd('structuredClone') // Output: 14.714s

// cloneDeep Test

console.time('cloneDeep')

for (let i = 0; i < 10000000; i++) {
  _.cloneDeep(objToClone)
}

console.timeEnd('cloneDeep') // Output: 4.540s

I thought it was worth trying again with a non circular object, something more representative of what you might commonly come across in real life usage, unfortunately it was still about half the speed:

const _ = require('lodash')

const objToClone = {
  name: 'Bob',
  age: 28,
  sibblings: [
    'Laura',
    'Rachel',
    'Mike',
    'Steve'
  ],
  pet: {
    type: 'dog',
    name: 'Fido',
    age: 6,
    likes: [
      {
        type: 'food',
        name: 'Pedigree Tender Bites'
      },
      {
        type: 'toy',
        name: 'Ball'
      }
    ]
  }
}

// structuredClone Test

console.time('structuredClone')

for (let i = 0; i < 10000000; i++) {
  structuredClone(objToClone)
}

console.timeEnd('structuredClone') // Output: 39.020s

// cloneDeep Test

console.time('_.CloneDeep')

for (let i = 0; i < 10000000; i++) {
  _.cloneDeep(objToClone)
}

console.timeEnd('_.CloneDeep') // Output: 23.668s

// JSON.parse(JSON.stringify()) Test

console.time('JSON.parse')

for (let i = 0; i < 10000000; i++) {
  JSON.parse(JSON.stringify(objToClone))
}

console.timeEnd('JSON.parse') // Output: 27.438s

Note that Lodash was also marginally faster than JSON.parse(JSON.stringify())

What structuredClone() Can And Can’t Handle

So it’s worth mentioning that structuredClone() also has it’s limitations in what it can clone. According to the MDN docs it can’t handle functions, DOM nodes, and certain object properties. For example the following code won’t work:

const func = () => 'I\'m a function!'

const copy = structuredClone(func)

console.log('copy ==>', copy)

// Output:
// DOMException [DataCloneError]: () => 'I\'m a function!' could not be cloned.

Now, to be fair, this also doesn’t work correctly using the Lodash _.deepClone() either, which instead of throwing an error just converts the function to an empty object:

const _ = require('lodash')

const func = () => 'I\'m a function!'

const copy = _.cloneDeep(func)

console.log('copy ==>', copy) // Output: copy ==> {}

// And no, you can't do copy() to call the function

The Lodash docs don’t really give any info about what it can and can’t copy so unfortunately this is a bit of a guessing game. For example, given the above, you might think that the below wouldn’t work, but it does…

const _ = require('lodash')

class person {
  constructor(name) {
    this.name = name
  }

  getName() {
    return this.name
  }
}

const objToClone = {
  classInstance: new person('John')
}

console.log('_.cloneDeep ==>', _.cloneDeep(objToClone).classInstance.getName())
// Output: _.cloneDeep ==> John

// structuredClone would return an error

And just to be clear, I did check, the class instance that is returned by the lodash _.deepClone() is a copy and not just a reference.

Conclusion

Honestly, I’m underwhelmed by structuredClone() I was expecting better performance and not only was it slower but it also failed where lodash succeeded when it came to cloning instantiated classes.

I was also surprised that in the test where JSON.parse(JSON.stringify()) was also an option, Lodash still seemed to be quicker. This wasn’t exactly an exhaustive test, I don’t know if this holds true for larger or more complex objects, but it’s still interesting.

TL;DR It looks like I’ll be sticking with Lodash for awhile longer.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.