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

[core] Optimise destructuring for useState, useReducer #16842

Merged
merged 2 commits into from
Aug 9, 2019
Merged

[core] Optimise destructuring for useState, useReducer #16842

merged 2 commits into from
Aug 9, 2019

Conversation

NMinhNguyen
Copy link
Contributor

@NMinhNguyen NMinhNguyen commented Aug 1, 2019

Update

GitHub repo now available at https://github.com/NMinhNguyen/babel-plugin-transform-destructuring


I used babel/babel#9486 as an inspiration which was originally intended for Create React App (facebook/create-react-app#5602 (comment)). You can actually see selectiveLoose option used in CRA's Babel config although I don't think it does anything as the Babel PR isn't merged. This PR uses patch-package which you may not like as maintainers, but I'm mostly just raising this PR to look at the bundle size diff 😅. When I built packages using #16072 (as well as in my own project at work), I observed that it doesn't seem to handle array destructuring, thus this PR.

This PR uses my fork (source on unpkg.com) of @babel/plugin-transform-destructuring with the following diff:

Show diff
diff --git a/node_modules/@babel/plugin-transform-destructuring/README.md b/../babel/node_modules/@babel/plugin-transform-destructuring/README.md
index 4c866eab2..92df503a1 100644
--- a/node_modules/@babel/plugin-transform-destructuring/README.md
+++ b/../babel/node_modules/@babel/plugin-transform-destructuring/README.md
@@ -1,3 +1,37 @@
+==================
+
+**This is a (hopefully temporary) fork of `@babel/plugin-transform-destructuring` that optimises array destructuring of `useState` and `useReducer` React Hooks.**
+
+**It only includes these two changes:**
+
+```diff
+@@ -1,6 +1,10 @@
+ import { declare } from '@babel/helper-plugin-utils';
+ import { types as t } from '@babel/core';
+
++const forcedLoose = ['useReducer', 'useState'].map(
++  name => new RegExp(`^_(?:React\\$)?${name}\\d*$`)
++);
++
+ export default declare((api, options) => {
+   api.assertVersion(7);
+```
+
+```diff
+@@ -129,7 +133,11 @@ export default declare((api, options) => {
+     }
+
+     toArray(node, count) {
+-      if (this.arrayOnlySpread || (t.isIdentifier(node) && this.arrays[node.name])) {
++      if (
++        this.arrayOnlySpread ||
++        (t.isIdentifier(node) &&
++          (this.arrays[node.name] || forcedLoose.some(regex => regex.test(node.name))))
++      ) {
+```
+
+==================
+
 # @babel/plugin-transform-destructuring
 
 > Compile ES2015 destructuring to ES5
diff --git a/node_modules/@babel/plugin-transform-destructuring/lib/index.js b/../babel/node_modules/@babel/plugin-transform-destructuring/lib/index.js
index 70e546780..8ef1de471 100644
--- a/node_modules/@babel/plugin-transform-destructuring/lib/index.js
+++ b/../babel/node_modules/@babel/plugin-transform-destructuring/lib/index.js
@@ -25,6 +25,10 @@ function _core() {
   return data;
 }
 
+const forcedLoose = ['useReducer', 'useState'].map(
+  name => new RegExp(`^_(?:React\\$)?${name}\\d*$`)
+);
+
 var _default = (0, _helperPluginUtils().declare)((api, options) => {
   api.assertVersion(7);
   const {
@@ -134,7 +138,11 @@ var _default = (0, _helperPluginUtils().declare)((api, options) => {
     }
 
     toArray(node, count) {
-      if (this.arrayOnlySpread || _core().types.isIdentifier(node) && this.arrays[node.name]) {
+      if (
+        this.arrayOnlySpread ||
+        (_core().types.isIdentifier(node) &&
+          (this.arrays[node.name] || forcedLoose.some(regex => regex.test(node.name))))
+      ) {
         return node;
       } else {
         return this.scope.toArray(node, count);
diff --git a/node_modules/@babel/plugin-transform-destructuring/package.json b/../babel/node_modules/@babel/plugin-transform-destructuring/package.json
index 91d8c52b3..05d4f771e 100644
--- a/node_modules/@babel/plugin-transform-destructuring/package.json
+++ b/../babel/node_modules/@babel/plugin-transform-destructuring/package.json
@@ -1,6 +1,6 @@
 {
-  "name": "@babel/plugin-transform-destructuring",
-  "version": "7.5.0",
+  "name": "@minh.nguyen/plugin-transform-destructuring",
+  "version": "7.5.2",
   "description": "Compile ES2015 destructuring to ES5",
   "repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-destructuring",
   "license": "MIT",

Bundle size diff can be viewed here. An example is provided below for convenience:

-var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));

  var _React$useState = _react.default.useState(false),
-     _React$useState2 = (0, _slicedToArray2.default)(_React$useState, 2),
-     expanded = _React$useState2[0],
-     setExpanded = _React$useState2[1];
+     expanded = _React$useState[0],
+     setExpanded = _React$useState[1];

I think this PR could be merged and then if (or more like when) babel-plugin-optimize-react is ready at some point in the future, this can be replaced with it.

@mui-pr-bot
Copy link

mui-pr-bot commented Aug 1, 2019

@material-ui/core: parsed: -0.29% 😍, gzip: -0.39% 😍
@material-ui/lab: parsed: -0.34% 😍, gzip: -0.44% 😍

Details of bundle changes.

Comparing: c5b518b...d33e0b7

bundle parsed diff gzip diff prev parsed current parsed prev gzip current gzip
@material-ui/core -0.29% -0.39% 329,729 328,783 90,219 89,871
@material-ui/core/Paper 0.00% +0.02% 🔺 68,684 68,684 20,469 20,473
@material-ui/core/Paper.esm 0.00% -0.02% 62,058 62,058 19,209 19,205
@material-ui/core/Popper -2.46% -2.46% 29,185 28,468 10,434 10,177
@material-ui/core/Textarea -11.65% -9.95% 5,766 5,094 2,372 2,136
@material-ui/core/TrapFocus 0.00% -0.19% 3,834 3,834 1,617 1,614
@material-ui/core/styles/createMuiTheme -0.02% +0.03% 🔺 16,389 16,386 5,824 5,826
@material-ui/core/useMediaQuery -20.92% -19.22% 3,213 2,541 1,311 1,059
@material-ui/lab -0.34% -0.44% 152,627 152,113 46,584 46,381
@material-ui/styles 0.00% +0.03% 🔺 51,401 51,401 15,285 15,289
@material-ui/system -0.05% +0.25% 🔺 15,666 15,658 4,350 4,361
Button -0.92% -0.98% 79,421 78,687 24,279 24,041
Modal -4.68% -4.28% 15,050 14,346 5,233 5,009
Portal -18.78% -15.94% 3,579 2,907 1,568 1,318
Rating -0.97% -0.91% 70,735 70,047 22,079 21,878
Slider -0.97% -0.99% 75,066 74,338 23,271 23,041
colorManipulator 0.00% 0.00% 3,904 3,904 1,543 1,543
docs.landing 0.00% 0.00% 52,124 52,124 13,837 13,837
docs.main -0.17% -0.25% 591,584 590,557 188,926 188,445
packages/material-ui/build/umd/material-ui.production.min.js -0.08% -0.01% 300,030 299,786 86,234 86,228

Generated by 🚫 dangerJS against d33e0b7

@NMinhNguyen
Copy link
Contributor Author

Hmm, surely this can't be right? How can the parsed size be exactly the same and the gzip size increase? Paper doesn't seem to use any Hooks either 🤔

bundle parsed diff gzip diff prev parsed current parsed prev gzip current gzip
@material-ui/core/Paper 0.00% +0.02% 🔺 68,673 68,673 20,470 20,474

azure-pipelines.yml Outdated Show resolved Hide resolved
@eps1lon
Copy link
Member

eps1lon commented Aug 1, 2019

Hmm, surely this can't be right?

Like different chunking of webpack. Don't bother with every byte fluctuation.

@NMinhNguyen
Copy link
Contributor Author

@eps1lon that makes sense. What's your thoughts on the approach in this PR though? 🙂

@eps1lon
Copy link
Member

eps1lon commented Aug 1, 2019

It would be nice to add some tests to the babel plugin which have input/output code. I'm a bit surprised this lowered parsed and gzip size. babel-plugin-optimize-react tried to reduce bundle size as well but only for parsed size while gzip size went up.

@NMinhNguyen
Copy link
Contributor Author

It would be nice to add some tests to the babel plugin which have input/output code.

No guarantees as to when I'll be able to get to that, but just curious if you've taken a look at the bundle size diff? The reason babel-plugin-optimize-react doesn't produce similar results is because I think it was mostly only able to extract React.default.createElement into its own variable. Let me prepare the diff to show you what I mean.

@eps1lon
Copy link
Member

eps1lon commented Aug 1, 2019

if you've taken a look at the bundle size diff?

Well, yes

I'm a bit surprised this lowered parsed and gzip size.

and yes:
#16072

@NMinhNguyen
Copy link
Contributor Author

Here's the diff generated from your PR: https://github.com/NMinhNguyen/material-ui/commit/7682e1809a596c7778583be9a07143735bde2cbf At first glance it's actually hard to tell whether gzip or parsed size should go up or down, whereas with the toSlicedArray removal across the board, you'd naturally think that it leads to a reduction 🙂

@eps1lon
Copy link
Member

eps1lon commented Aug 1, 2019

At first glance it's actually hard to tell whether gzip or parsed size should go up or down

Most of the time code that minifies better increases gzip size since both use similar compression methods.

Again: Include tests (acts as documentation) for input/output and explain what you do differently than babel-plugin-optimize-react and why. IMO this should be a standalone plugin that is subject to peer review outside of the material-ui bubble since it is concerned with react not material-ui specifically. Unless I'm missing something.

@oliviertassinari
Copy link
Member

IMO this should be a standalone plugin that is subject to peer review outside of the material-ui bubble since it is concerned with react not material-ui specifically. Unless I'm missing something.

Strong 👍

@NMinhNguyen
Copy link
Contributor Author

NMinhNguyen commented Aug 1, 2019

explain what you do differently than babel-plugin-optimize-react and why

I have commented on this PR as well as in the code itself that I copied @babel/plugin-transform-destructuring and added an extra check to check for useState and useReducer and even pointed out the exact lines that were added. Is that insufficient?

IMO this should be a standalone plugin that is subject to peer review outside of the material-ui bubble since it is concerned with react not material-ui specifically. Unless I'm missing something.

Although I do agree with the idea, this has been added as a private package so we can iterate quickly.

Update

I have now added a README and tests, hopefully this addresses your concerns.

@eps1lon
Copy link
Member

eps1lon commented Aug 1, 2019

Is that insufficient?

You added this later without mentioning it. I'm not checking every comment (especially the opening description) in a thread when the thread is updated. Blame github but not me.

Although I do agree with the idea, this has been added as a private package so we can iterate quickly.

But this is not an incubator for babel plugins. I appreciate your work and I feel some sense of pride that that our current size tracking solution is useful for quick iterations. But why did you not propose this for the original plugin? The original authors are better equipped in reviewing this.

I guess you used a draft to indicate that this is just a showcase and not meant to be merged? Basically to make the case for this change when proposing it to the original plugin?

@NMinhNguyen
Copy link
Contributor Author

NMinhNguyen commented Aug 1, 2019

I'm not checking every comment (especially the opening description) in a thread when the thread is updated. Blame github but not me.

My apologies, I assumed that I had updated the opening description before you got the chance to review this PR. Although I'm pretty confident those individual comments on the code were there shortly after I raised the PR.

But why did you not propose this for the original plugin?

Because there is already babel/babel#9486 that has been there since 10 February. I don't think it actually works though, but I did lift the regexes used in that PR and with some debugging of @babel/plugin-transform-destructuring, have been able to narrow down where it decides whether to emit toSlicedArray, and added an additional guard there. I personally think that it's hard to make it general-purpose and work with the original plugin, but we can apply a heuristic which checks for variable names like _useState2 and assume it's from React - at least in the context of the MUI monorepo. I think babel-plugin-optimize-react does a more robust check (it checks it was actually imported from react) but that would definitely be outside the scope of @babel/plugin-transform-destructuring.

I guess you used a draft to indicate that this is just a showcase and not meant to be merged?

I used it as a draft to share an idea with the team and to get feedback on the approach. And hopefully to get it merged (after some refinement perhaps?) and eventually replaced by babel-plugin-optimize-react. I don't think I'll be proposing a change to the original plugin.

I've spoken to @oliviertassinari offline to avoid back and forth and this is what he proposed:

  1. The scope of the plugin is Material-UI. Supporting the broader ecosystem would be significantly harder. This plugin has zero ambition in this area. It will be dropped as soon as the community has come up with a viable alternative (in other words, when babel-plugin-optimize-react is stable)
  2. Test are present in case we need to do small changes in the future. Who knows what's going to happen when we release the next version.
  3. The plugin is always run, in dev and in the tests. If the tests are green running with it, it's likely safe.

Point 3 is what I'm gonna verify now. Although in this PR, I made sure that I wasn't including this plugin where it wasn't being included before - it was only being included as part of @babel/preset-env.

@NMinhNguyen
Copy link
Contributor Author

The plugin is always run, in dev and in the tests. If the tests are green running with it, it's likely safe.

I've verified your Babel configuration and can confirm that this plugin is added as long as BABEL_ENV !== 'es' https://github.com/mui-org/material-ui/blob/af13581127278bf295c64cacb9ed101f00e76262/babel.config.js#L6 It is only === 'es' when you do build:es: https://github.com/mui-org/material-ui/blob/af13581127278bf295c64cacb9ed101f00e76262/packages/material-ui/package.json#L27

In other words, it's included in all cases apart from when you output ES modules (without any ES transpilation whatsoever): this covers things like dev and tests.

@NMinhNguyen
Copy link
Contributor Author

Just realised there was a separate config for docs, so I've added the plugin there as well.

Before

Screen Shot 2019-08-01 at 15 14 00

After

Screen Shot 2019-08-01 at 15 17 17

@NMinhNguyen
Copy link
Contributor Author

NMinhNguyen commented Aug 1, 2019

The main downside to manually including this plugin is that prior to this PR, this plugin would be added by preset-env based on your .browserslistrc (e.g. IE11 needs the destructuring transform) - or in other words, conditionally. Instead, what we can do is use patch-package to patch the destructuring plugin in place instead of including it manually. Another advantage of patch-package is that the diff would be a lot easier to digest: https://github.com/NMinhNguyen/material-ui/commit/a9c9d1dd6f597c9187b57e9c2aa0b65b6d413fef#diff-d8abe5330dd7f524a33c63b6fedf39dd

Furthermore, patch-package is actually quite bulletproof and if you (or a bot) bump the version of a patched dependency and it fails to apply the patch, it will throw an error and prevent the install from proceeding. As you can see from the filename, it keeps track of the exact version that was patched as well. The patch is safe to use because it's a dev dependency - we're not requiring users of MUI to also apply the patch. We're simply taking on the burden of optimising the JS modules that are shipped to npm.

@oliviertassinari
Copy link
Member

Instead, what we can do is use patch-package to patch the destructuring plugin in place instead of including it manually.

I like that approach. We used to have it for a react docgen issue. It could be the cleanest option. @eps1lon What do you think?

Also, I really like the bundle size reduction on the small universal modules, like the -20% on useMediaQuery.

@eps1lon
Copy link
Member

eps1lon commented Aug 1, 2019

Applying unofficial patches was only every used temporary and never? on master. Unless these patches are reviewed by original maintainers they become a maintenance hazard. Maybe babel has to fix a bug tomorrow that creates a merge conflict with this patch? Maybe this patch introduces a subtle bug. Maybe it has a performance impact. There are just dozens of potential issues inherent to changes to the build setup that are just not worth it saving 1kB in a 300kB bundle.

I still don't understand how this is material-ui specific? Are we using useState or useReducer differently? Please file a PR upstream, if nothing happens publish the fork so that it is subject to peer review, file a PR applying the published package and this is likely to get applied. See babel-plugin-optimize-clsx created by @merceyz

@NMinhNguyen
Copy link
Contributor Author

I struggle to see how this is different in principle to e.g. you overriding react-docgen’s default behaviour by supplying a custom parser which isn’t necessarily blessed by the maintainers? Is your issue with not understanding the code change? Because I am happy to explain and walk you through it. React’s monorepo has a bunch of Babel plugins that aren’t necessarily React-specific (e.g. wrapping warning calls in an if (__DEV__) check or using object-assign instead of Object.assign) and they’re not made available publicly, but they’re using them because they solve some problem.

Lastly, if this for some reason doesn’t work out (which I don’t think is the case here given the tests and how simple the change is), you could just revert this commit and keep innovating?

@josgraha
Copy link
Contributor

josgraha commented Aug 1, 2019

@eps1lon I get all of your arguments and they make sense from a maintainers perspective but
sometimes you have to take the users perspective and ask how this benefits them in terms of TTI and load-time performance, are the tradeoffs better for them? I think the 1KB in 300KB assertion sounded a bit off given the bundle size reductions above.

@eps1lon
Copy link
Member

eps1lon commented Aug 2, 2019

I struggle to see how this is different in principle to e.g. you overriding react-docgen’s default behaviour by supplying a custom parser which isn’t necessarily blessed by the maintainers?

One is patching existing code without running that patch through the tests of the original code. The other is using public APIs.

Lastly, if this for some reason doesn’t work out (which I don’t think is the case here given the tests and how simple the change is), you could just revert this commit and keep innovating?

By that argument we should merge anything and "just" revert if it doesn't work out.

you have to take the users perspective and ask how this benefits them in terms of TTI and load-time performance, are the tradeoffs better for them?

Well then I should ask this question: How big are the TTI and load-time performance increases? You could start by measuring the deploy preview.

I think the 1KB in 300KB assertion sounded a bit off given the bundle size reductions above.

bundle parsed diff gzip diff prev parsed current parsed prev gzip current gzip
@material-ui/core -0.29% -0.37% 329,630 328,682 90,147 89,815

That was not my assertion but a measurement taken. What do you mean by "off". It's 1052 bytes. I took the liberty and rounded that to 1kB.

@oliviertassinari
Copy link
Member

oliviertassinari commented Aug 2, 2019

By that argument we should merge anything and "just" revert if it doesn't work out.

Make me think of our decision making process. There is some truth in that. I think that for a decision that is easily reversible and looks good on paper, we should explore them without hesitation (meaning, take them as fast as we can). What does look good on paper mean? Well, I think that it has to look worth our time, a good positive ROI compared to how we would spend our time otherwise.

I don't want to influence this issue, I want to only cover an aspect on how I think that we should make decisions on Material-UI.

@eps1lon
Copy link
Member

eps1lon commented Aug 2, 2019

Compromise proposal:

optimize-react does not integrate well with preset-env. If I pipe optimize-react to preset-env I get no slideToArray helper:

Good:

Both at the same time introduces slicedToArray:
Bad:

I'm going to work on fixing optimize-react since interoperability with preset-env is in general important for us. Maybe there are other optimizations that we're missing out on.

@NMinhNguyen
Copy link
Contributor Author

NMinhNguyen commented Aug 2, 2019

By that argument we should merge anything and "just" revert if it doesn't work out.

I think you're extending the argument too far. I would argue that in the presence of factors such as

  • bundle diff
  • passing tests
  • working deploy previews
  • tests for the Babel transform itself
  • single commit
  • 20% reduction on certain individual modules (useMediaQuery)

this change is actually not that risky. There have been far riskier changes that have been merged in the history of this project.

in: source, transform: optimize-react, preset-env (optimize-react seems to not have been applied)

optimize-react's array destructuring optimisation wasn't applied but var __reactCreateElement__ = _react["default"].createElement; was as evidenced in one of the earlier comments already.

Also you're still yet to comment on the actual code change being applied to the destructuring plugin.

@eps1lon
Copy link
Member

eps1lon commented Aug 2, 2019

was as evidenced in one of the earlier comments already.

The scope was the destructuring. You're getting a bit too eager nitpicking my arguments.

@NMinhNguyen
Copy link
Contributor Author

@eps1lon Are you able to comment on the actual code change itself and what your concerns are exactly? Are you not interested in me walking you through the code change as well?

@eps1lon
Copy link
Member

eps1lon commented Aug 2, 2019

I didn't even look at the code changes (since marked as Draft). All of my concerns are with the process itself. The implementation is probably fine.

yarn.lock Show resolved Hide resolved
@NMinhNguyen NMinhNguyen marked this pull request as ready for review August 8, 2019 08:44
Copy link
Member

@oliviertassinari oliviertassinari left a comment

Choose a reason for hiding this comment

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

Do you have a public repository for this package https://www.npmjs.com/package/@minh.nguyen/plugin-transform-destructuring?

@NMinhNguyen
Copy link
Contributor Author

@oliviertassinari not yet, see #16842 (comment)

@oliviertassinari
Copy link
Member

oliviertassinari commented Aug 8, 2019

I invite you to review the package by installing the two side-by-side using git diff 🙂 I'll see if I can set up CI, GitHub etc on the weekend and then publish 7.5.3, but no guarantees as I'm a little busy these days.

Ok, cool, we can wait a week or two. I don't think that we need to rush the changes.

@NMinhNguyen
Copy link
Contributor Author

@oliviertassinari done https://github.com/NMinhNguyen/babel-plugin-transform-destructuring (and PR description updated with a link)

@oliviertassinari
Copy link
Member

oliviertassinari commented Aug 9, 2019

@NMinhNguyen Thanks, I have submitted a pull request to the repository. I have noticed that you don't watch it, nor that the project has an issues tab enabled nor that the CI is setup. If you have no intention to keep it up to date with babel-plugin-transform-destructuring upstream and to guarantee a minimum level of quality. I think that our best option is to use patch-package, so we

  • can get upstream fixes, and if it conflicts with the changes here in the future, we can kill them.
  • can have more audit visibility on the alterations, it's safer.

@NMinhNguyen
Copy link
Contributor Author

NMinhNguyen commented Aug 9, 2019

@oliviertassinari thanks. It wasn’t intentional - I simply forked Babel’s repo (preserving the Git history for the plugin). It’s my first proper public repo so I’m not familiar with the set up, I was just trying to do the bare minimum that you requested (create a repo which I did and thought was sufficient). I’ll enable issues and review your PR.

That being said, I don’t care too much whether you wanna use the published fork or patch-package. I’m fine with either approach, just let me know and I’ll amend my PR

Copy link
Member

@oliviertassinari oliviertassinari left a comment

Choose a reason for hiding this comment

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

Thanks, I have created a personal reminder for checking babel-plugin-transform-destructuring every couple of months. Let's hope this patch is only temporary :). We will revisit the approach in the future if it causes issues.

@oliviertassinari oliviertassinari merged commit ae3f9f8 into mui:master Aug 9, 2019
@oliviertassinari
Copy link
Member

@NMinhNguyen Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants