◆ structuredClone は高速にクローンできると期待してたけど遅かった
◆ データが小さめだと JSON 化してパースするよりも遅い
◆ JSON 化できるデータのみが対象なら JavaScript で再帰的にオブジェクトを見てコピーしたほうが速い

Node.js で structuredClone が使えるようになると以前書きましたがブラウザでも Chrome 98 から使えるようになってました
https://chromestatus.com/feature/5630001077551104

クローンのための関数なので JSON 化してパースする

JSON.parse(JSON.stringify(obj))

よりはるかに速いよねと期待してたのですが 使ってみると思った以上に遅かったです
こんな感じのコードで試しました

<!DOCTYPE html>

<script>
const clone0 = x => JSON.parse(JSON.stringify(x))
const clone1 = x => structuredClone(x)
const clone2 = x => {
if (typeof x === "object") {
if (x === null) return x
if (Array.isArray(x)) return x.map(clone2)
const obj = {}
for (const [k, v] of Object.entries(x)) {
obj[k] = clone2(v)
}
return obj
} else {
return x
}
}

const data = {
a: 1,
b: null,
c: true,
d: "a",
e: [1],
f: {
g: 1,
h: [1, { i: 3, j: "a", k: [[[4]]] }],
l: {},
},
m: { n: { o: { p: 0 } } },
}

const measure = (f) => {
const before = performance.now()
for (let i = 0; i < 10000; i++) {
f(data)
}
const after = performance.now()
return +(after - before).toFixed(3)
}

const results = [[], [], []]
for (let i = 0; i < 3; i++) {
results[0].push(measure(clone0))
results[1].push(measure(clone1))
results[2].push(measure(clone2))
results[0].push(measure(clone0))
results[1].push(measure(clone1))
results[2].push(measure(clone2))
results[0].push(measure(clone0))
results[1].push(measure(clone1))
results[2].push(measure(clone2))
results[0].push(measure(clone0))
results[1].push(measure(clone1))
results[2].push(measure(clone2))
}
console.log(results)
</script>

clone0: JSON 化してパース
clone1: structuredClone
clone2: JavaScript で再帰的にオブジェクトを見てコピー

となってます

それぞれクローン時にコピーされる対象が多少は違いますが基本的な JSON 化できる部分に限れば同じになるはずです
JavaScript の方法では プリミティブ値はそのまま代入でコピーし Symbol も対象になります
関数は参照をコピーするので実体は同じです
配列やオブジェクトの場合は再帰的に上記の処理を行い getter/setter や prototype は無視します

対象にするデータは小さめだけどちょっとは複雑な構造で

const data = {
a: 1,
b: null,
c: true,
d: "a",
e: [1],
f: {
g: 1,
h: [1, { i: 3, j: "a", k: [[[4]]] }],
l: {},
},
m: { n: { o: { p: 0 } } },
}

というデータにしています
このデータに対して 1 万回のクローンをそれぞれの方法でやってみて比較します
単純に for 文でループだと最適化都合で順番の影響があるときもあるので 直接何度か繰り返して書いてます

結果はこうなりました

[
[
68.2,
46.8,
40.1,
40.1,
41.6,
42.4,
41.4,
41.7,
44.2,
42.2,
43.1,
41.9
],
[
128.7,
78,
73.6,
75.6,
79.3,
74.7,
75.7,
74.9,
79.5,
75.7,
76.7,
77.7
],
[
35.4,
22.6,
22.9,
22.2,
23.5,
22,
21.1,
23.1,
22.9,
20.9,
23.1,
21.8
]
]

なんと structuredClone は JSON 化してパースよりも遅いです
最速は JavaScript で再帰してコピーする方法です

それぞれの方法で最初だけ遅いとか structuredClone の方法で ループの最初の 1, 5, 9 回目が少し遅いとかは Chrome の最適化都合でよくあることです

データによるところがあると思いますし 大きいデータになると結果が変わるということはありえそうです
実際のデータを想定して Github API のデータを使ってみます
リポジトリ検索の結果です
https://api.github.com/search/repositories?q=js

私が取得したタイミングのデータでは 175KB ありました
データ量が多いのでループ回数は 100 回にして 再度計測してみました

[
[
106.7,
105.1,
102.4,
101.2,
104.7,
103.9,
103.5,
103,
100.9,
101.9,
104.3,
101.6
],
[
64.1,
64.3,
78.5,
63.1,
63.6,
65.3,
79.1,
63.5,
64,
63.7,
64.1,
77.1
],
[
34.5,
30.4,
30.5,
29.9,
30.3,
29.8,
32.3,
30.1,
30,
31.1,
29.9,
32.5
]
]

JSON 化してパースが最も遅くなりました
最速が JavaScript で処理というのは一緒です

structuredClone 関数が追加されれば高速なクローンが手軽に扱えると期待していましたが 小さいデータでは JSON 化してパースするよりも遅いという結果でした
グローバル関数なので気軽にクローンに使えますが 高速なものではないと覚えておいたほうがいいかもです



JSON 化できるものという条件で書いたので JavaScript で再帰してコピーする方法では循環参照を考慮してないです
入れると無限に再起してコールスタックの上限を超えたとエラーになります
そこもチェックするようにするともう少し JavaScript で再帰してコピーする方法の速度が落ちますが Github API のデータの場合で 2,3ms 程度だったので結果としてはほぼ変わりなしでした