-
-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use spread syntax for shallow clones #8
Conversation
properties: [ | ||
{type: 'SpreadElement', argument: {type: 'Identifier', name: 'b'}} | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this test is about the classic runtime.
Old behaviour: Pass a single JSX spread element by reference
New behavour: Always pass a shallow clone, no exception for a single JSX spread element.
The old behaviour is was consistent with Babel for the classic runtime. The new behaviour is consistent with Babel for the automatic runtime, as well as TypeScript for both the classic and the automatic runtime.
I like the new behaviour, but I’m open to changing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn’t it be nicer not to generate throwaway objects and run faster? :'(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends.
I think React creates shallow clones internally anyway, so it’s safe. For other frameworks it may be unsafe to pass objects by reference. However, I haven’t seen any issues with this in practice, which makes me think it’s probably fine to pass by reference.
I also suspect JavaScript engines optimize this anyway, but this is just a guess.
Another difference is that the spread syntax filters own properties.
I think it would be best to keep the new spread syntax, but I’ll gladly change it if you have strong feelings about passing by reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
think
may
probably
suspect
All the more reason why maybe a benchmark is a good idea! I think as we are generating code, generating fast code for everyone is worth it.
We know that not creating useless objects is fast.
Creating useless objects might be fine in V8. Maybe fine or not in other places. But that’s guesses.
Generating nice code is also nice: definitely one of the goals too. But like, the current code isn’t terrible, but I’m worried that it’s getting a small bit nicer at a perf cost.
That being said I modeled the code after Babel, which presumably has done some perf work, but I didn’t. And I made sure MDX 2 was fast. But might be good to investigate the perf here regardless?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO this isn’t about performance. It’s about if we want to make a special case of passing one single JSX spread.
/** @jsx h */
class Person {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
getFullName() {
return this.firstName + ' ' + this.lastName
}
}
function h(tagName, props) {
console.log(props.getFullName())
}
// Logs 'Spongebob Squarepants'
<tag {...new Person('Spongebob', 'Squarepants')} />
// Uncaught TypeError: props.getFullName is not a function
<tag {...new Person('Spongebob', 'Squarepants')} additional="prop" />
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think it is important to make sure that the code this project generates performs well?
That being said, your change only affects components that already receive a spread. Most things in MDX are “plain” elements. Very few receive spreads. So in my tests on an MB of MDX which contains markdown and a couple of “HTML” elements, only 2 things changed (_jsx(MDXLayout,
and _jsx(_createMdxContent,
), which did not affect runtime performance at all.
So performance of this PR is good, this point is resolved.
But your last example raises an interesting point, which might be a breaking change: this code can throw errors where previously it didn’t.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe good to benchmark the code? Is that something you’d be interested in checking?
I wonder about speed.
properties: [ | ||
{type: 'SpreadElement', argument: {type: 'Identifier', name: 'b'}} | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn’t it be nicer not to generate throwaway objects and run faster? :'(
I did some const object = {a: 'a'}
/** @type {unknown[]} */
const results = []
globalThis.results = results
const runCount = 1e6
console.log(runCount, 'runs')
/*********************************************************************************/
console.log()
console.log('Rest last / new key')
console.time('Spread')
for (let i = 0; i < runCount; i++) {
results.push({[i]: i, ...object})
}
console.timeEnd('Spread')
console.time('Object.assign')
for (let i = 0; i < runCount; i++) {
results.push(Object.assign({[i]: i}, object))
}
console.timeEnd('Object.assign')
/*********************************************************************************/
console.log()
console.log('Rest first / new key')
console.time('Spread')
for (let i = 0; i < runCount; i++) {
results.push({...object, [i]: i})
}
console.timeEnd('Spread')
console.time('Object.assign')
for (let i = 0; i < runCount; i++) {
results.push(Object.assign({}, object, {[i]: i}))
}
console.timeEnd('Object.assign')
/*********************************************************************************/
console.log()
console.log('Rest last / same key')
console.time('Spread')
for (let i = 0; i < runCount; i++) {
results.push({b: i, ...object})
}
console.timeEnd('Spread')
console.time('Object.assign')
for (let i = 0; i < runCount; i++) {
results.push(Object.assign({b: i}, object))
}
console.timeEnd('Object.assign')
/*********************************************************************************/
console.log()
console.log('Rest first / same key')
console.time('Spread')
for (let i = 0; i < runCount; i++) {
results.push({...object, b: i})
}
console.timeEnd('Spread')
console.time('Object.assign')
for (let i = 0; i < runCount; i++) {
results.push(Object.assign({}, object, {b: i}))
}
console.timeEnd('Object.assign')
/*********************************************************************************/
const objectB = {b: 'b'}
const objectC = {c: 'c'}
console.log()
console.log('Multiple existing')
console.time('Spread')
for (let i = 0; i < runCount; i++) {
results.push({...object, ...objectB, ...objectC})
}
console.timeEnd('Spread')
console.time('Object.assign')
for (let i = 0; i < runCount; i++) {
results.push(Object.assign({}, object, objectB, objectC))
}
console.timeEnd('Object.assign')
/*********************************************************************************/
console.log()
console.log('Multiple mixed')
console.time('Spread')
for (let i = 0; i < runCount; i++) {
results.push({...object, x: 'x', ...objectB, y: 'y', ...objectC})
}
console.timeEnd('Spread')
console.time('Object.assign')
for (let i = 0; i < runCount; i++) {
results.push(Object.assign({}, object, {x: 'x'}, objectB, {y: 'y'}, objectC))
}
console.timeEnd('Object.assign') Node.js v19.9.0 results:
Google Chrome 114 produces similar results. Firefox 114 results:
Epiphany 42 (WebKitGTK 2.38.6) results:
I was surprised by the situations where console.log()
console.log('Multiple mixed')
console.time('Spread')
for (let i = 0; i < runCount; i++) {
results.push({x: 'x', ...objectB, y: 'y', ...objectC})
}
console.timeEnd('Spread')
console.time('Object.assign')
for (let i = 0; i < runCount; i++) {
results.push(Object.assign({x: 'x'}, objectB, {y: 'y'}, objectC))
}
console.timeEnd('Object.assign') This produces:
I think this shows both spread syntax and |
I compared the differences around spreads that are generated by this project currently, the PR, Babel, and TS:
Your proposal matches TS and it matches Babel (auto). There are two things in your proposal: I think a) is fine. b) is probably fine. |
Interestingly, Babel’s behavour depends on the If this is disabled (default), it passes by reference. If enabled, it creates a shallow copy. Given the following code: <div {...props} /> Using the following module.exports = {
plugins: [
[
'@babel/plugin-transform-react-jsx',
{useSpread: true}
]
]
} Yields: /*#__PURE__*/React.createElement("div", {
...props
}); Using the following module.exports = {
plugins: [
[
'@babel/plugin-transform-react-jsx',
// Default
{useSpread: false}
]
]
} Yields: /*#__PURE__*/React.createElement("div", props); SWC always passes by reference for the classic runtime, and a shallow copy for the automatic runtime. ESBuild always creates a shallow copy, just like TypeScript I think this shows generally that most tools / situations result in a shallow copy. I also found some discussion on Babel’s implementation in babel/babel#11419. |
Likely, both SWC and I were inspired by Babel, but did not care about the legacy of “useSpread” or other weird options. Interesting thread! It makes sense that frameworks want a mutable copy that they own, shallowly. But as it isn’t like that yet, no framework can assume that, so it’s rather useless now. How do you assess the risk of potentially breaking someone? I agree it’s a tiny chance. |
I guess I can just do a breaking change as I’m doing majors anyway! |
I don’t expect this to break anyone’s code, but there is a small chance. I think I’d make it a major release just to be sure. Also I’d drop Node.js 14 support while at it. |
This affects the assignment of `_components`. For JSX see syntax-tree/estree-util-build-jsx#8
This affects the assignment of `_components`. For JSX see syntax-tree/estree-util-build-jsx#8
This comment has been minimized.
This comment has been minimized.
released, thanks! :) |
Initial checklist
Description of changes
Use object spread syntax for creating shallow clones instead of
Object.assign()
.Refs mdx-js/mdx#2310