Shallow clone vs. deep clone, in Node, with benchmark

By admin
A very common way to create a "copy" of an Object in

JavaScript
ORG

is to copy all things from

one
CARDINAL

object into an empty one. Example:

const original = { foo : "

Foo
PERSON

" } const copy = Object . assign ({}, original) copy. foo = "Bar" console . log ([original. foo , copy. foo ])

This outputs

[ ‘

Foo
PERSON

‘ , ‘Bar’ ]

Obviously the problem with this is that it’s a shallow copy, best demonstrated with an example:

const original = { names : [ "

Peter
PERSON

" ] } const copy = Object . assign ({}, original) copy. names . push ( "

Tucker
PERSON

" ) console . log ([original. names , copy. names ])

This outputs:

[ [ ‘

Peter
PERSON

‘ , ‘

Tucker
PERSON

‘ ], [ ‘

Peter
PERSON

‘ , ‘

Tucker
PERSON

‘ ] ]

which is arguably counter-intuitive. Especially since the variable was named "copy".

Generally, I think Object.assign({}, someThing) is often a red flag because if not

today
DATE

, maybe in some future the thing you’re copying might have mutables within.

The "solution" is to use structuredClone which has been available since

Node 16
LAW

. Actually, it was introduced within minor releases of

Node 16
LAW

, so be a little bit careful if you’re still on

Node 16
LAW

.

Same example:

const original = { names : [ "

Peter
PERSON

" ] }; const copy

= structuredClone
ORG

(original); copy. names . push ( "

Tucker
PERSON

" ); console . log ([original. names , copy. names ]);

This outputs:

[ [ ‘

Peter
PERSON

‘ ], [ ‘

Peter
PERSON

‘ , ‘

Tucker
PERSON

‘ ] ]

Another deep copy solution is to turn the object into a string, using

JSON.stringify
PRODUCT

and turn it back into a (deeply copied) object using JSON.parse . It works like structuredClone but full of caveats such as unpredictable precision loss on floating point numbers, and not to mention date objects ceasing to be date objects but instead becoming strings.

Benchmark

Given how much "better" structuredClone is in that it’s more intuitive and therefore less dangerous for sneaky nested mutation bugs. Is it fast? Before even running a benchmark; no, structuredClone is slower than Object.assign({}, …) because of course. It does more! Perhaps the question should be: how much slower is structuredClone ? Here’s my benchmark code:

import fs from "fs" import assert from "assert" import

Benchmark
ORG

from "benchmark" const obj = JSON . parse (fs.

readFileSync
DATE

( "package-lock.json" , "utf8" )) function f1 ( ) { const copy = Object . assign ({}, obj) copy. name = "else" assert (copy. name !== obj. name ) } function f2 ( ) { const copy = structuredClone (obj) copy. name = "else" assert (copy. name !== obj. name ) } function f3 ( ) { const copy = JSON .

parse
PERSON

( JSON .

stringify
PERSON

(obj)) copy. name = "else" assert (copy. name !== obj. name ) } new Benchmark . Suite () . add ( "f1" , f1) . add ( "f2" , f2) . add ( "f3" , f3) . on ( "cycle" , ( event ) => { console . log (

String
PERSON

(event. target )) }) . on ( "complete" , function ( ) { console . log ( "

Fastest
ORG

is " + this . filter ( "fastest" ). map ( "name" )) }) . run ()

The results:

❯ node assign-or-clone.js f1 x

8,057,542
CARDINAL

ops/

sec
ORG


±0.84%
PERCENT

(

93
CARDINAL

runs sampled) f2 x

37,245
CARDINAL

ops/sec

±0.68%
PERCENT

(

94
CARDINAL

runs sampled) f3 x

37,978
CARDINAL

ops/sec

±0.85%
PERCENT

(

92
CARDINAL

runs sampled)

Fastest
ORG

is f1

In other words, Object.assign({}, …) is

200
CARDINAL

times faster than structuredClone .

By the way, I re-ran the benchmark with a much smaller object (using the package.json instead of the package-lock.json ) and then Object.assign({}, …) is

only 20
CARDINAL

times faster.

Mind you! They’re both ridiculously fast in the grand scheme of things.

If you do this…

for ( let i = 0 ; i < 10 ; i++) { console . time ( "f1" ) f1 () console . timeEnd ( "f1" ) console . time ( "f2" ) f2 () console . timeEnd ( "f2" ) console . time ( "f3" ) f3 () console . timeEnd ( "f3" ) }

the last bit of output of that is:

f1: 0.006ms f2:

0.06ms
CARDINAL

f3:

0.053ms
CARDINAL

which means that it took

0.06 milliseconds
TIME

for

structuredClone
ORG

to make a convenient deep copy of an object that is

5
CARDINAL

KB as a JSON string.

Conclusion