Help us understand the problem. What is going on with this article?

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

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

比較対象

  1. JSON.stringify/JSON.parse
  2. Nodeビルトインモジュールv8のserialize/deserialize
  3. lodashのcloneDeep
    npm GitHub Repo stars
  4. deepcopy - deep copy data
    npm GitHub Repo stars
  5. clone - offers foolproof deep cloning of objects, arrays, numbers, strings, maps, sets, promises, etc. in JavaScript.
    npm GitHub Repo stars
  6. clone-deep - Recursively (deep) clone JavaScript native types, like Object, Array, RegExp, Date as well as primitives.
    npm GitHub Repo stars
  7. rfdc - Really Fast Deep Clone
    npm GitHub Repo stars

比較結果

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

JavaScriptディープコピーベンチマーク.002.png

JavaScriptディープコピーベンチマーク.003.png

JavaScriptディープコピーベンチマーク.004.png

検証に用いたコード

ベンチマークを出すのに用いたコードを示しておきます。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は、ゆるやかに準拠しているとしています。これは、MapDateErrorなどもコピーできますが、関数やシンボルは無理です。再帰的なオブジェクトにも対応しています。

deepcopyはかなり頑張っていて、関数やシンボルまでコピーできるそうです。

所感

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

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

suin
Qiita 4位/TypeScript入門書執筆中/TypeScripterのための座談会「YYTypeScript」主催/『実践ドメイン駆動設計』書籍邦訳レビュア/分報Slack考案/YYPHP主催/CodeIQマガジン執筆/株式会社クラフトマンソフトウェア創設/Web自動テスト「ShouldBee」の開発/TypeScript/DDD/OOP
https://yyts.connpass.com/
craftsman_software
「インフラの心配は、もうおしまい」 インフラ運用を自動化し、手作業を限りなくゼロにする会社
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away