JavaScriptのディープコピー速さ比較 〜7つの手法/ライブラリを比べてみた〜

この投稿では、JavaScript(Node.js)でディープコピーするにあたって使えるコードスニペットやライブラリの処理速度を比較した結果をお見せします。

## 比較対象

1. JSON.stringify/JSON.parse
- Nodeビルトインモジュールv8の[serialize/deserialize](https://nodejs.org/api/v8.html#v8_v8_serialize_value)
- lodashの[cloneDeep](https://www.npmjs.com/package/lodash.clonedeep)<br>
  ![npm](https://img.shields.io/npm/dw/lodash)
  ![GitHub Repo stars](https://img.shields.io/github/stars/lodash/lodash?style=social)
- [deepcopy](https://www.npmjs.com/package/deepcopy) - deep copy data<br>
  ![npm](https://img.shields.io/npm/dw/deepcopy)
  ![GitHub Repo stars](https://img.shields.io/github/stars/sasaplus1/deepcopy.js?style=social)
- [clone](https://www.npmjs.com/package/clone) - offers foolproof deep cloning of objects, arrays, numbers, strings, maps, sets, promises, etc. in JavaScript.<br>
  ![npm](https://img.shields.io/npm/dw/clone)
  ![GitHub Repo stars](https://img.shields.io/github/stars/pvorb/clone?style=social)
- [clone-deep](https://www.npmjs.com/package/clone-deep) - Recursively (deep) clone JavaScript native types, like Object, Array, RegExp, Date as well as primitives.<br>
  ![npm](https://img.shields.io/npm/dw/clone-deep)
  ![GitHub Repo stars](https://img.shields.io/github/stars/jonschlinkert/clone-deep?style=social)
- [rfdc](https://www.npmjs.com/package/rfdc) - Really Fast Deep Clone<br>
  ![npm](https://img.shields.io/npm/dw/rfdc)
  ![GitHub Repo stars](https://img.shields.io/github/stars/davidmarkclements/rfdc?style=social)

## 比較結果

比較結果の棒グラフです。棒が長いほうが速いです。

![JavaScriptディープコピーベンチマーク.002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/05679831-fab1-d3d6-8b15-6125382683ca.png)

![JavaScriptディープコピーベンチマーク.003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/6bb5235c-1d44-7fe9-a83c-4f129e4c7bdd.png)

![JavaScriptディープコピーベンチマーク.004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/4dbde246-20a7-3d09-82f9-319465a49bd5.png)

## 検証に用いたコード

ベンチマークを出すのに用いたコードを示しておきます。[GitHub](https://github.com/suin/nodejs-playground/tree/master/deepclone)にも置いてあるので、検証してみたい方はそちらをお使いください。

__「32プロパティ持つオブジェクト」の検証コード__

```js

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」の検証コード__

```js
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段ネストしたオブジェクト」の検証コード__

```js
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)](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)に準拠しているものとしては、v8モジュールの`serialize`/`deserialize`です。lodashの`cloneDeep`は、ゆるやかに準拠しているとしています。これは、`Map`や`Date`、`Error`などもコピーできますが、関数やシンボルは無理です。再帰的なオブジェクトにも対応しています。

[deepcopy](https://www.npmjs.com/package/deepcopy)はかなり頑張っていて、関数やシンボルまでコピーできるそうです。

## 所感

* JSONはものすごく遅いわけではないので、オブジェクトの性質によっては使い所は大いにありそう。
* 構造化複製アルゴリズムを使いたいなら、v8よりlodashが良さそう。
* rfdcめっちゃ速い。

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:→[Twitter@suin](https://twitter.com/suin)