この投稿では、JavaScript(Node.js)でディープコピーするにあたって使えるコードスニペットやライブラリの処理速度を比較した結果をお見せします。
比較対象
- JSON.stringify/JSON.parse
- Nodeビルトインモジュールv8のserialize/deserialize
- lodashのcloneDeep
-
deepcopy - deep copy data
-
clone - offers foolproof deep cloning of objects, arrays, numbers, strings, maps, sets, promises, etc. in JavaScript.
-
clone-deep - Recursively (deep) clone JavaScript native types, like Object, Array, RegExp, Date as well as primitives.
-
rfdc - Really Fast Deep Clone
比較結果
比較結果の棒グラフです。棒が長いほうが速いです。
検証に用いたコード
ベンチマークを出すのに用いたコードを示しておきます。GitHubにも置いてあるので、検証してみたい方はそちらをお使いください。
「32プロパティ持つオブジェクト」の検証コード
const Benchmark = require('benchmark')
const report = require('beautify-benchmark')
const v8 = require('v8')
const _ = require('lodash')
const deepcopy = require('deepcopy')
const clone = require('clone')
const cloneDeep = require('clone-deep')
const rfdc = require('rfdc')()
const value = {}
for (let i = 0; i < 32; i++) {
value[`prop${i}`] = 0
}
console.log(value)
const json = () => JSON.parse(JSON.stringify(value))
const v8_ = () => v8.deserialize(v8.serialize(value))
const lodash = () => _.cloneDeep(value)
const deepcopy_ = () => deepcopy(value)
const clone_ = () => clone(value)
const cloneDeep_ = () => cloneDeep(value)
const rfdc_ = () => rfdc(value)
const suite = new Benchmark.Suite()
suite
.add(`json`, json)
.add(`v8`, v8_)
.add(`lodash`, lodash)
.add(`deepcopy`, deepcopy_)
.add(`clone`, clone_)
.add('clone-deep', cloneDeep_)
.add('rfdc', rfdc_)
.on('cycle', event => report.add(event.target))
.on('complete', () => report.log())
.run()
「32要素持つArray」の検証コード
const Benchmark = require('benchmark')
const report = require('beautify-benchmark')
const v8 = require('v8')
const _ = require('lodash')
const deepcopy = require('deepcopy')
const clone = require('clone')
const cloneDeep = require('clone-deep')
const rfdc = require('rfdc')()
const value = new Array(32).fill(0)
console.log(value)
const json = () => JSON.parse(JSON.stringify(value))
const v8_ = () => v8.deserialize(v8.serialize(value))
const lodash = () => _.cloneDeep(value)
const deepcopy_ = () => deepcopy(value)
const clone_ = () => clone(value)
const cloneDeep_ = () => cloneDeep(value)
const rfdc_ = () => rfdc(value)
const suite = new Benchmark.Suite()
suite
.add(`json`, json)
.add(`v8`, v8_)
.add(`lodash`, lodash)
.add(`deepcopy`, deepcopy_)
.add(`clone`, clone_)
.add('clone-deep', cloneDeep_)
.add('rfdc', rfdc_)
.on('cycle', event => report.add(event.target))
.on('complete', () => report.log())
.run()
「32段ネストしたオブジェクト」の検証コード
const Benchmark = require('benchmark')
const report = require('beautify-benchmark')
const v8 = require('v8')
const _ = require('lodash')
const deepcopy = require('deepcopy')
const clone = require('clone')
const cloneDeep = require('clone-deep')
const rfdc = require('rfdc')()
let node = {}
const value = node
for (let i = 0; i < 32; i++) {
node.child = {}
node = node.child
}
console.log(value)
const json = () => JSON.parse(JSON.stringify(value))
const v8_ = () => v8.deserialize(v8.serialize(value))
const lodash = () => _.cloneDeep(value)
const deepcopy_ = () => deepcopy(value)
const clone_ = () => clone(value)
const cloneDeep_ = () => cloneDeep(value)
const rfdc_ = () => rfdc(value)
const suite = new Benchmark.Suite()
suite
.add(`json`, json)
.add(`v8`, v8_)
.add(`lodash`, lodash)
.add(`deepcopy`, deepcopy_)
.add(`clone`, clone_)
.add('clone-deep', cloneDeep_)
.add('rfdc', rfdc_)
.on('cycle', event => report.add(event.target))
.on('complete', () => report.log())
.run()
注意点
各コピー方法によって、MapやDateのようなビルトインオブジェクトそのままコピーできたり、関数までコピーできたり、再帰的なオブジェクトまでコピーできたり、と仕様がまちまちなのでご注意ください。
例えば、JSON.stringify
+JSON.parse
は再帰的なオブジェクトに使おうとするとエラーになることが知られています。
構造化複製アルゴリズム(structured clone algorithm)に準拠しているものとしては、v8モジュールのserialize
/deserialize
です。lodashのcloneDeep
は、ゆるやかに準拠しているとしています。これは、Map
やDate
、Error
などもコピーできますが、関数やシンボルは無理です。再帰的なオブジェクトにも対応しています。
deepcopyはかなり頑張っていて、関数やシンボルまでコピーできるそうです。
所感
- JSONはものすごく遅いわけではないので、オブジェクトの性質によっては使い所は大いにありそう。
- 構造化複製アルゴリズムを使いたいなら、v8よりlodashが良さそう。
- rfdcめっちゃ速い。
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→Twitter@suin