💭

オブジェクトをコピーして編集する方法はどれがよさそうか?

2023/09/20に公開

結論: structuredClone でコピーしてオブジェクトを編集する のがパフォーマンス面からみてもバランスがとれていそう。

検証したかったこと

以下のようなオブジェクトをコピーして深い階層にあるプロパティを変更したいとき、どのやり方が最速なのか?を調べたかった。

const obj = {
  prop: { sub: { val: '' } },
  prop2: { sub: { val: '' } },
};

コピー方法の候補はこちら

  • スプレッド演算子で頑張る
  • structuredClone を使う
  • immer を使う
  • JSON.parse(JSON.stringify(obj)) [1]

なお、今回は全部をディープコピーするわけではなく、変更しない箇所はシャローコピーで良いものとする。つまり、prop2 はシャローコピーでも良い。実務で遭遇したのがそういうケースだったので。

検証に使った HTML ファイル(全文ママ)
<!DOCTYPE html>

<script type="module">
  import { produce } from 'https://unpkg.com/immer@10.0.2/dist/immer.production.mjs';
  const obj = {
    prop: { sub: { val: '' } },
    prop2: { sub: { val: '' } },
  };

  console.time('raw');
  for (let i = 0; i < 1e5; i++) {
    const copied = {
      ...obj,
      prop: { ...obj.prop, sub: { ...obj.prop.sub, val: `${i}_dayo` } },
    };
  }
  console.timeEnd('raw');

  console.time('structuredClone');
  for (let i = 0; i < 1e5; i++) {
    const copied = structuredClone(obj);
    copied.prop.sub.val = `${i}_dayo`;
  }
  console.timeEnd('structuredClone');

  console.time('immer');
  for (let i = 0; i < 1e5; i++) {
    const copied = produce(obj, (draft) => {
      draft.prop.sub.val = `${i}_dayo`;
    });
  }
  console.timeEnd('immer');

  console.time('JSON.stringify');
  for (let i = 0; i < 1e5; i++) {
    const copied = JSON.parse(JSON.stringify(obj));
    copied.prop.sub.val = `${i}_dayo`;
  }
  console.timeEnd('JSON.stringify');
</script>

結果

macOS の Chrome で計測した。以下のようになった。

方法 タイム
raw 8.72290039062 ms
JSON.stringify 47.80322265625 ms
structuredClone 116.84326171875 ms
immer 152.74682617875 ms

もうちょいプロパティを増やしたらこのようになった。

方法 タイム
raw 11.43896484 ms
JSON.stringify 277.97060546 ms
immer 279.44091796 ms
structuredClone 343.45703125 ms

考察

obj のサイズによらず、 スプレッド演算子で地道にコピーするやり方が最も早い ようだった。
…と言いたいところだが、増やしたプロパティはシャローコピーでも OK という要件だった。これが原因で raw だけそこまで速度が変わらなかったのかもしれない。

スプレッド演算子以外のやり方は、obj のサイズに依存している部分が大きそう。ただ、 スプレッド演算子以外の3つはオーダーはほぼ同じだった ので、どれを使うかは好みの問題と言えそう。

結論: structuredClone がバランスが取れていて良さそう

まあ色々書いたが、パフォーマンスのボトルネックにならなければ structuredClone を使うのが一番良い ように思う。コードもシンプルになり、パフォーマンスもいうほど悪くない。モダンブラウザもほぼ structuredClone に対応している。なかなかバランスが取れた良い選択肢だと思う。

脚注
  1. Date 型を保持できないなど問題があるので、このやり方はあまり使うべきではない。例えば JSON.parse(JSON.stringify(new Date())) をしてみると、得られる結果は string 型の値になる ↩︎

Discussion