Repository navigation
Improve performance of structuredClone #50320
Description
Activity
- addedfeature requestIssues requesting new Node.js features.Issues requesting new Node.js features.
on Oct 21, 2023 Your report isn't actionable unless you have concrete improvement suggestions. "Increase throughput" isn't.
In case you're unaware: structuredClone is implemented in terms of V8's value serializer/deserializer API, i.e., the meat of the work is done inside V8, not node.
@nodejs/performance we could consider a fast path for certain types of values if that's an interesting use case.
I really doubt that once you handle all the edge cases it's going to be significantly faster than what V8 already does.
For example, OP's deepClone function doesn't handle arrays with non-element properties correctly, to say nothing of recursive data structures.
@bnoordhuis not going through an extra MessageChannel might be a good start https://github.com/nodejs/node/blob/main/lib/internal/structured_clone.js but even so - I wouldn't assume V8's implementation is particularly fast, that they optimized it a lot or that it's too difficult to beat (in a spec compliant manner obviously), I may be wrong.
Oh sure, no disagreement from me there, but OP's suggestion is to shortcut the algorithm and I don't think that's going to be a fruitful endeavor. Adding cycle detection alone probably wipes out much of the performance advantage, never mind everything else.
(I recently wrote a golang library that speaks V8's serde protocol so this is all sparking fresh on my mind.)
Reacted by Yagiz NizipliI would expect more often than not the input is going to be a complex object, not a primitive which a fast path could be added for. I don't think there's much we can do here beyond pulling out the clone algorithm so you don't need to construct a the MessageChannel to pass through it, and then start looking at what can be done in V8.
One could possibly inform the V8 optimizer with shape information to compute ahead if other code passed in a shape known to not have pitfalls like circular references and skip the checks. That would be a complicated effort though. I'm happy to mentor anyone wanting to take it on when I can, just be prepared for it to take several weeks or even months. 😅
I wrote up a simplified implementation of structuredClone in native and it improves the performance a bit:
confidence improvement accuracy (*) (**) (***) misc/structured-clone.js n=10000 type='arraybuffer' *** 5.93 % ±1.64% ±2.18% ±2.84% misc/structured-clone.js n=10000 type='object' *** 7.83 % ±1.89% ±2.53% ±3.32% misc/structured-clone.js n=10000 type='string' *** 43.98 % ±1.90% ±2.53% ±3.29%Would be even better if we just don't use ports. But baby steps. That would also allow us to support structuredClone in customized snapshots (I am fairly certain that there was an issue for this - cannot find it though - or someone asked me about this).
Reacted by Jacob Morrison and Benjamin GruenbaumThanks for getting that change in so quickly @joyeecheung. Do you think this issue should be closed and a separate one should be reopened for future work?
With the snippet in the OP I got this:
StructuredClone at 100 iterations: 0.804ms StructuredClone at 10000 iterations: 56.326ms StructuredClone at 1000000 iterations: 5.372s StructuredClone at 10000000 iterations: 52.784s ParseStringify at 100 iterations: 0.617ms ParseStringify at 10000 iterations: 41.563ms ParseStringify at 1000000 iterations: 4.063s ParseStringify at 10000000 iterations: 41.037s ShallowClone1 at 100 iterations: 0.434ms ShallowClone1 at 10000 iterations: 6.873ms ShallowClone1 at 1000000 iterations: 409.999ms ShallowClone1 at 10000000 iterations: 4.105s DeepClone at 100 iterations: 0.618ms DeepClone at 10000 iterations: 16.654ms DeepClone at 1000000 iterations: 1.308s DeepClone at 10000000 iterations: 13.063sSo I think there are still work to be done to make structured clone more performant. One is #50330 (comment) and another is using fast APIs when there's no transfer list. But I think even with those there's still a lot for structured clone to catch up with other alternatives.
Reacted by Jacob Morrison and Sukka- added a commit that references this issue
on Feb 9, 2024 github-actions commented
on Apr 23, 2024 on Apr 23, 2024 – with GitHub ActionsContributorMore actionsThere has been no activity on this feature request for 5 months. To help maintain relevant open issues, please add the never-stale
Issues and PRs exempt from automated stale handling. label or close this issue if it should be closed. If not, the issue will be automatically closed 6 months after the last non-automated comment.
For more information on how the project manages feature requests, please consult the feature request management document.Reacted by Jacob Morrison- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Apr 23, 2024 github-actions commented
on May 23, 2024 on May 23, 2024 – with GitHub ActionsContributorMore actionsThere has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.
For more information on how the project manages feature requests, please consult the feature request management document.
I used iso-bench for the benchmark to avoid possible optimization pollution:
const { IsoBench } = require("iso-bench"); var x = { "userId": "134374683951759360", "guild": "86908735326281728", "status": "online", "activities": [{ "name": "Twitch", "type": 1, "url": "https://www.twitch.tv/craggle25", "details": "Weekend dirt naps with @mart0k and Cat. Hunt Showdown", "state": "Hunt: Showdown", "applicationId": null, "timestamps": null, "party": null, "assets": { "largeText": null, "smallText": null, "largeImage": "twitch:craggle25", "smallImage": null }, "flags": 0, "emoji": null, "buttons": [], "createdTimestamp": 1697880668811 }], "clientStatus": { "desktop": "online" } }; function isArr(x) { return Array.isArray(x); } function isObject(x) { return typeof x === 'object'; } function shallowClone(obj) { let clone = {}; for (let key in obj) { let r = obj[key]; let isArray = isArr(r); if (!isArray && isObject(r)) { continue; } if (isArray) { clone[key] = []; for (let i = 0; i < r.length; i++) { clone[key][i] = shallowClone(r[i]); } } else { clone[key] = r; } } return clone; } // pulled this from https://github.com/nodejs/node/issues/34355#issuecomment-658394617 function deepClone(o) { if (typeof o !== "object") { return o } if (!o) { return o } // https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 if (Array.isArray(o)) { const newO = [] for (let i = 0; i < o.length; i += 1) { const val = !o[i] || typeof o[i] !== "object" ? o[i] : deepClone(o[i]) newO[i] = val === undefined ? null : val } return newO } const newO = {} for (const i of Object.keys(o)) { const val = !o[i] || typeof o[i] !== "object" ? o[i] : deepClone(o[i]) if (val === undefined) { continue } newO[i] = val } return newO } new IsoBench("Clone object") .add("StructuredClone", () => structuredClone(x)) .add("ParseStringify", () => JSON.parse(JSON.stringify(x))) .add("ShallowClone1", () => shallowClone(x)) .add("DeepClone", () => deepClone(x)) .consoleLog() .run();
The result is not surprising btw:

What is the problem this feature will solve?
structuredCloneperformance is quite slow compared to alternative object copying(while those also have their downsides in some capacity, the performance loss is worth polyfilling at the moment).I also noticed in the source code there's already a TODO about performance and it would also have WebStreams ReadableStream
There was another discussion here which I thought was insightful #34355 (comment)
Consider code like the following:
The output gives me:
What is the feature you are proposing to solve the problem?
Increase throughput on
structuredClonecallsWhat alternatives have you considered?
Polyfilling my own object clone methods