Skip to content
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

Refactor React.Children to reduce indirection #18332

Merged
merged 12 commits into from
Mar 23, 2020

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Mar 17, 2020

This has been bugging me for a while. I know we don't plan to change their behavior because it's a legacy pattern anyway (although we don't have an alternative either). But it's a bit awkward to see this indirection in the bundle. It's also very confusing to read.

The original code was written assuming this is perf critical and we must avoid closures. I don't think it is. This pattern isn't super common and when you have it, you probably don't want to iterate over thousands of these anyway. In either case, my last implementation has minimal use of closures too. I removed pooling too which is a bit over-engineered for this use case and might actually hurt perf. Unlike with events, it's not observable.

I re-implemented count, forEach, and toArray in terms of map. I know they have slightly different semantics so a naïve implementation wouldn't work but I think I got it right (and when I did it wrong, tests failed). The original rationale to implement them separately was to reuse a lower level primitive that doesn't require any extra allocations. But since both forEach and count have other quirks (like counting holes) I don't expect them to actually be used that much in critical paths anymore. So I think it's okay count allocates an array now and later throws it away.

Each commit is fairly mechanical. At the end, I have a single recursive function as a core of map, and other functions expressed through map. Instead of a tangled web of indirections. Read individual commits. Although I guess reading the whole thing also works because you can follow it now.

@facebook-github-bot facebook-github-bot added the React Core Team Opened by a member of the React Core Team label Mar 17, 2020
@codesandbox-ci
Copy link

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 98902bb:

Sandbox Source
broken-darkness-3269g Configuration

@sizebot
Copy link

sizebot commented Mar 18, 2020

Details of bundled changes.

Comparing: 2666642...98902bb

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js -2.8% -2.2% 108.97 KB 105.95 KB 26.03 KB 25.45 KB UMD_DEV
react.production.min.js -3.6% -3.1% 12.66 KB 12.21 KB 4.87 KB 4.72 KB UMD_PROD
react.profiling.min.js -2.8% -2.3% 16.2 KB 15.75 KB 5.95 KB 5.81 KB UMD_PROFILING
react.development.js -4.1% -3.3% 70.39 KB 67.52 KB 18.27 KB 17.67 KB NODE_DEV
react.production.min.js -5.8% -4.6% 7.73 KB 7.28 KB 2.97 KB 2.84 KB NODE_PROD
React-dev.js -3.8% -3.3% 75.59 KB 72.69 KB 19.11 KB 18.48 KB FB_WWW_DEV
React-prod.js -8.6% -6.2% 18.02 KB 16.47 KB 4.66 KB 4.37 KB FB_WWW_PROD
React-profiling.js -8.6% -6.2% 18.02 KB 16.47 KB 4.66 KB 4.37 KB FB_WWW_PROFILING

React: size: -3.6%, gzip: -3.1%

Size changes (experimental)

Generated by 🚫 dangerJS against 98902bb

@sizebot
Copy link

sizebot commented Mar 18, 2020

Details of bundled changes.

Comparing: 2666642...98902bb

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js -3.0% -2.6% 100.31 KB 97.29 KB 24.65 KB 24.01 KB UMD_DEV
react.production.min.js -3.9% -3.0% 11.59 KB 11.15 KB 4.56 KB 4.42 KB UMD_PROD
react.profiling.min.js -3.0% -2.6% 15.13 KB 14.68 KB 5.67 KB 5.53 KB UMD_PROFILING
react.development.js -4.6% -3.5% 62.14 KB 59.27 KB 16.92 KB 16.33 KB NODE_DEV
react.production.min.js -6.8% -5.3% 6.53 KB 6.08 KB 2.65 KB 2.51 KB NODE_PROD
React-dev.js -3.8% -3.2% 76.26 KB 73.37 KB 19.28 KB 18.65 KB FB_WWW_DEV
React-prod.js -8.5% -6.3% 18.08 KB 16.53 KB 4.67 KB 4.37 KB FB_WWW_PROD
React-profiling.js -8.5% -6.3% 18.08 KB 16.53 KB 4.67 KB 4.37 KB FB_WWW_PROFILING

React: size: -3.9%, gzip: -3.0%

Size changes (stable)

Generated by 🚫 dangerJS against 98902bb

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to take a lot of work to figure out if this is semantically equivalent in every way.

These have such subtle differences between them that needs to be replicated.

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 18, 2020

I'm pretty sure I'm right (c)

I can write a small one-off generative test thing to verify the results match between implementations. #covid #bored

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 18, 2020

I know it's subtle though. E.g. people likely depend even on the nested map unwrapping call order. I think I kept all of that but I agree it's worth testing more closely.

@sophiebits
Copy link
Collaborator

wdyt of just dropping the pooling and closure avoidance but keeping traverseAllChildren with a different callback for map vs forEach vs count? I think that gets you 90% of the code simplification.

@sophiebits
Copy link
Collaborator

and is nicer in some ways – eg: count doesn't need to make an array.

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 18, 2020

I guess that would be better than what we have today, but I'd still prioritize golfing code size over runtime perf of these helpers.

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 18, 2020

Worth noting traverse perf was super important when it was shared by the reconciler code. It's not anymore though.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The escape algorithm is interesting. Several extra objects like regexps and closures per entry. Even for things like forEach and count that doesn't need the keys.

I couldn't find a bug after a few spot checks. I didn't carefully review it all but it's at least an indication that there's probably at least not many bugs.

Copy link
Collaborator

@sophiebits sophiebits left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how the code size is meaningfully different your way vs mine, but what you have looks reasonable enough.

@gaearon gaearon merged commit fc96a52 into facebook:master Mar 23, 2020
@gaearon
Copy link
Collaborator Author

gaearon commented Mar 23, 2020

Let's give it a try. The only discrepancy I found is that React.Children.forEach(null) no longer returns null. I didn't fix it because there's no logical reason why it should (this seems accidental), and relying on forEach return value is extremely unlikely (this is the only case where it's not undefined). I couldn't find any internal callsites doing it either.

@gaearon gaearon deleted the less-children branch March 23, 2020 22:57
@gaearon
Copy link
Collaborator Author

gaearon commented Mar 24, 2020

#18380

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants