Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Conversation

markthiessen
Copy link
Contributor

One of the big features in Webpack 2 is tree shaking. Modified config to enable this in client bundle output.
Debated adding logic to common config as server bundle still needs module transform, but in the end decided it was probably clearer for developers to just pull out module rules from common and have client and server config define separately.

@dnfclas
Copy link

dnfclas commented Feb 16, 2017

Hi @markthiessen, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!

This seems like a small (but important) contribution, so no Contribution License Agreement is required at this point. Real humans will now evaluate your PR.

TTYL, DNFBOT;

@DanHarman
Copy link
Contributor

I am wondering whether we couldn't keep the .babelrc and put the 'modules: false' into that as all it does is stop the transformation of ES6 modules into CommonJS modules. Not sure why that would cause problems for the serverside bundle?

As it stands its a bit inconsistent that the serverside build would use the .babelrc, but the client doesn't in this PR.

@markthiessen markthiessen force-pushed the enable-webpack-tree-shaking-reactredux branch from 4f869eb to 1e85b0e Compare March 9, 2017 20:37
@markthiessen
Copy link
Contributor Author

@DanHarman I think you're probably right.. I've modified the PR to just include the .babelrc change

@DanHarman
Copy link
Contributor

Awesome. Hopefully this will be merged in soon.

@DanHarman
Copy link
Contributor

DanHarman commented Mar 10, 2017

So I just tested this config, and I've got a feeling it doesn't offer much (any?) benefit with the build methodology in this template.

I think the fundamental problem is that 'webpack --config webpack.config.vendor.js', uses a config which explicitly sets entrys to the top levels of all the heavy packages. This means none of this stuff gets shaken at all, and in fact can't be as its not working out the true dependency chain based on what the app is actually using.

I'm now left wondering if this separation of vendor and app packages is actually a good trade off, as we miss out on potentially significant savings on package size due to tree shaking. At least for a 'release' build.

@SteveSandersonMS - thoughts?

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Mar 10, 2017

Thanks @markthiessen for suggesting this!

Like @DanHarman, I'm also not seeing any significant changes in the resulting bundle size though, whether or not we use the vendor bundle.

First, with the vendor bundle enabled (as it is by default in the template), I'm seeing:

  • Before this PR: vendor.js is 372 kB; main-client.js is 43.8 kB
  • After this PR: vendor.js is 372 kB; main-client.js is 49.7 kB

Second, with the vendor bundle disabled (i.e., by commenting out the DllReferencePlugin entry in clientBundleConfig's plugins array), I get:

  • Before this PR: main-client.js is 387 kB
  • After this PR: main-client.js is 385 kB

These are all production builds (i.e., with --env.prod), before gzipping (which would probably cut a further 75% off the actual size going over the network, independently of tree-shaking).

So although it does very slightly reduce the size in the non-vendor-split case, it's not the sort of dramatic effect that people usually hype up about tree shaking. Is it possible that further build config changes are needed for it to really strip out large parts of the third-party libraries that never get called?

@DanHarman
Copy link
Contributor

DanHarman commented Mar 10, 2017

Hi,

I just tested by removing the vendor bundle (thanks for explaining how to do that). I am seeing benefit now.

So I have a very large vendor bundle as currently swapping from bootstrap to semantic-ui so both being pulled in:

λ webpack --config webpack.config.vendor.js
Hash: 18469d22acbb001775eaf66083ccd1a7528a5586
Version: webpack 2.2.1
Child
    Hash: 18469d22acbb001775ea
    Time: 37617ms
                                   Asset     Size  Chunks                    Chunk Names
    674f50d287a8c48dc19ba404d20fe713.eot   166 kB          [emitted]
    912ec66d7572ff821749319396470bde.svg   444 kB          [emitted]  [big]
    b06871f281fee6b241d60582ae9369b9.ttf   166 kB          [emitted]
    89889688147bd7575d6327160d64e760.svg   109 kB          [emitted]
                               vendor.js  3.39 MB       0  [emitted]  [big]  vendor
                              vendor.css  1.15 MB       0  [emitted]  [big]  vendor
Child
    Hash: f66083ccd1a7528a5586
    Time: 30403ms
                                   Asset     Size  Chunks                    Chunk Names
    674f50d287a8c48dc19ba404d20fe713.eot   166 kB          [emitted]
    89889688147bd7575d6327160d64e760.svg   109 kB          [emitted]
    912ec66d7572ff821749319396470bde.svg   444 kB          [emitted]  [big]
    b06871f281fee6b241d60582ae9369b9.ttf   166 kB          [emitted]
                               vendor.js  3.84 MB       0  [emitted]  [big]  vendor

Now using this vendor bundle my project is as follows:

λ webpack
(node:26372) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic, see https://github.com/webpack/loader-utils/issues/56
parseQuery() will be replaced with getOptions() in the next major version of loader-utils.
Hash: 201f0a6f1fd844fe780ec5a5fddbe09935c5ded0
Version: webpack 2.2.1
Child
    Hash: 201f0a6f1fd844fe780e
    Time: 10099ms
                 Asset      Size  Chunks             Chunk Names
        main-client.js   74.9 kB       0  [emitted]  main-client
              site.css   1.56 kB       0  [emitted]  main-client
    main-client.js.map   36.8 kB       0  [emitted]  main-client
          site.css.map  85 bytes       0  [emitted]  main-client
Child
    Hash: c5a5fddbe09935c5ded0
    Time: 10078ms
             Asset    Size  Chunks             Chunk Names
    main-server.js  126 kB       0  [emitted]  main-server

without vendor bundle:

λ webpack
(node:23800) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic, see https://github.com/webpack/loader-utils/issues/56
parseQuery() will be replaced with getOptions() in the next major version of loader-utils.
Hash: 18e18a507826b46cedb7c5a5fddbe09935c5ded0
Version: webpack 2.2.1
Child
    Hash: 18e18a507826b46cedb7
    Time: 11815ms
                 Asset      Size  Chunks                    Chunk Names
        main-client.js    1.2 MB       0  [emitted]  [big]  main-client
              site.css   1.56 kB       0  [emitted]         main-client
    main-client.js.map   1.37 MB       0  [emitted]         main-client
          site.css.map  85 bytes       0  [emitted]         main-client
Child
    Hash: c5a5fddbe09935c5ded0
    Time: 10134ms
             Asset    Size  Chunks             Chunk Names
    main-server.js  126 kB       0  [emitted]  main-server

So without tree shaking:

75kB (main-client.js) + 3.39MB (vendor.js) = c. 3.47Mb

With:

1,2MB (main-client.js) = 1.2MB

Now this is just a test project which is mostly just the templates code, so not sure how much would be saved with a 'real' app. i.e. right now 100% of semantic-ui-react is being dumped I would think.

On the plus side, the build time, given I'm using the cacheDirectory setting is actually still very good. approx 10 seconds with the client config, which is faster than without the cacheDirectory setting using the vendor config split out approach.

@DanHarman
Copy link
Contributor

DanHarman commented Mar 10, 2017

Did a further test with pretty much the bare template (i.e. no semantic-*) and vendor.js is about 2MB, if we go with a tree shaken no vendor approach the output is 1.2MB.

So does seem like a good saving.

I'm not sure whether the split is better or not in light of this. Obviously if framework is fairly static and the app iterates fast, then its a win to only be downloading a few k for the app. However we are looking at a pretty big overhead in total of not tree shaking.

I wonder if there is a way to tree shake the vendor.js based on what's in the client, whilst keeping it split out? Obviously this would be subject to change as new components were pulled in.

Forgot to say I did all my tests in Development...

@markthiessen
Copy link
Contributor Author

@SteveSandersonMS I think the results you included for with vendor dll enabled are without the --env.prod switch

These are the results I get:

Without prod switch

Before PR: running webpack results in main-client.js = 43.8 kB
After PR: running webpack results in main-client.js = 49.7 kB

With --env.prod
Before PR: running webpack --env.prod results in main-client.js = 16.7 kB
After PR: running webpack --env.prod results in main-client.js = 14.8 kB

It is a fairly small savings, true, but it's also a template with hardly any code :) It's slightly more pronounced in my project.

@DanHarman
Copy link
Contributor

@markthiessen perhaps I've misunderstood, but to get a major benefit from tree shaking we need to consider vendor.js as that's typically where the pruning would happen. i.e. we compare the client + vendor without shaking (because we can't shake the vendor modules when they are packaged separately) vs a much fatter client (which can shake out the vendor modules).

From my tests above, it does make a big difference, but I only ran them for dev builds.

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Mar 10, 2017

I think the results you included for with vendor dll enabled are without the --env.prod switch

Ah yes, thanks.

Did a further test with pretty much the bare template (i.e. no semantic-*) and vendor.js is about 2MB, if we go with a tree shaken no vendor approach the output is 1.2MB.

I'd like to be able to reproduce your results before we consider the effects. If you have a 2MB bundle, that suggests you're not minifying (not using --env.prod), which I don't think is the relevant case. The relevant case is production builds.

On the original template, these are the production build sizes I see:

  • Before PR, with vendor splitting enabled: vendor.js=372kB, main-client.js=16.7kB, total=388.7kB
  • After PR, with vendor splitting enabled: vendor.js=372kB, main-client.js=14.8kB, total=386.8kB
  • After PR, with vendor splitting disabled: main-client.js=385kB

So in all cases the differences are negligible, and certainly the advantages of vendor-splitting outweigh that sort of difference.

In what production-build cases does it makes a significant difference? Is it possible that we still don't have it configured correctly and tree-shaking should be making a bigger difference even on the default template?

It is a fairly small savings, true, but it's also a template with hardly any code :)

You should expect the effect to work the opposite way. The fact that the template is small should (theoretically) mean that tree-shaking produces much greater savings, because we're invoking less of the third-party code. As your application gets bigger, you'd expect tree-shaking to produce smaller savings, since you'll be referencing a larger proportion of third-party code paths.

@DanHarman
Copy link
Contributor

DanHarman commented Mar 10, 2017

Ok so I've rerun with --env.prod. Thanks for the correction there, still feeling my way out around nodeland!

Firstly I think I can confirm that tree shaking is enabled. With semantic-ui and semantic-ui-react pulled in, but not using any components I get:

  • Before PR: vendor.js = 672Kb, main-client.js = 16.9kB, total = 689Kb
  • After PR: main-client.js = 387kB, total = 387kB.

i.e. it has shaken out semantic-ui. However this is a completely artificial test as I need to use some of that module to see how it would fare under more realistic conditions.

n,b. my main-client.js is 2kb larger as I have another view and store in this code.

So perhaps the main take away here is that it really depends on what frameworks you are pulling in and how much of them you are using. The existing template is clearly factored pretty tightly without a great deal of anything to trim.

I don't see any downside to having the .babelrc change here, but we'd need more data to know whether dropping vendor.js is a net win for most, and it may be application specific. I guess it's something that could be in the docs, or a comment in the webpack.config.js?

Also another option is to just use `babel-preset-es2015-native-modules’

btw, and this may impact my results, are you guys having success running --env.prod on webpack.config.vendor.js as I get the issue mentioned here #715. Removing style-loader resolves it which is what I did here, but I don't currently understand the impact or purpose of style-loader here...

@SteveSandersonMS
Copy link
Member

I don't see any downside to having the .babelrc change here

Agreed. I'll merge this change, even if the benefits are very modest as far as we can tell. There doesn't seem to be any drawback and it might make a significant difference in some case.

we'd need more data to know whether dropping vendor.js is a net win for most, and it may be application specific

So far we don't have any examples of realistic cases where it would be a good idea not to do the vendor-splitting. If anyone finds themselves in a real-world case where it's best not to vendor-split, please let us know! Failing that, I think we're good as-is.

btw, and this may impact my results, are you guys having success running --env.prod on webpack.config.vendor.js as I get the issue mentioned here #715.

Yes, I was removing style-loader to make it work. Sorry for the inconvenience. Also #715 is now fixed in the dev branch so this problem will go away next time the templates are published.

@SteveSandersonMS SteveSandersonMS merged commit 3077b8a into aspnet:dev Mar 13, 2017
@hzoo
Copy link

hzoo commented Mar 14, 2017

FYI, babel-preset-es2015-native-modules was deprecated because the modules: false option in es2015 is the same thing.

I would also recommend trying out https://github.com/babel/babel-preset-env instead of es2015 😄. Let me know if you have questions (I made preset-env + maintain Babel)

You can also join our slack: http://slack.babeljs.io/

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

Successfully merging this pull request may close these issues.

5 participants