webpack: Tree shaking not working for ES module library
Bug report
What is the current behavior?
I’m importing a library (@Shopify/polaris https://github.com/Shopify/polaris-react) that’s quite large and I’m relying on Tree Shaking to remove unused code to ensure I don’t import the entire library. But webpack isn’t doing this. It’s importing the entire library (1.3MB before compression)
If the current behavior is a bug, please provide the steps to reproduce.
Here’s a small repo to reproduce the issue: https://github.com/dejesus2010/temp_repo_tree_shake
- Run
npm install
- Run
npm build
- Examine the opened webpack visualizer in the browser
What is the expected behavior?
Looking at the webpack visualizer output opened in the browser, I don’t expect @shopify/polaris-icons/index.js and @shopify/app-bride/* to be included in the vendor.chunk.js file. The entire @shopify/polaris/index.es.js should not be included either. I expect the TextContainer component (https://github.com/Shopify/polaris-react/tree/master/src/components/TextContainer) and its dependencies to be the only code imported from @Shopify/polaris. Its dependencies are very small (https://github.com/Shopify/quilt/tree/master/packages/css-utilities). Instead the entire library is imported (1.3MB before compression). In @shopify/polaris/index.es.js TextContainer is exported as a function.
Steps I’ve tried to resolve this issue:
-
Setting
sidEffects: false
inpackage.json
[1] and as a module rule [2]. I also ensuredoptimization.sideEffects: true
[3]. -
Ensure
mode
isproduction
. I’ve tried setting it in the webpack config file and as a command flag [1]. -
Ensure
@babel/preset-env {modules: false}
. From issue #660 this should be false by default anyways. -
I’ve followed the side-effects/tree-shaking example in the webpack repo: https://github.com/webpack/webpack/tree/master/examples/side-effects
Other relevant information: webpack version: 4.32.1 Node.js version: v12.3.1 Operating System: Mac OS Mojave 10.14.3 Additional tools:
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 5
- Comments: 17 (5 by maintainers)
The sideEffects optimization and usedExports optimization (aka Tree Shaking) are two different things.
sideEffects is much more effective since it allows to skip whole modules/files and the complete subtree.
usedExports relies on terser to detect side effects in statements but this is very difficult in javascript and not as effective. It also can’t skip subtree/dependencies since the spec says there side effects need to be evaluated. While exporting function works fine, react HOC are very problematic.
Let’s make an example:
The prebundled version looks like this:
When Button is unused you can effectively remove the
export { Button$1 };
which leaves all the remaining code. So the question is “Has this code side-effects or can it be safely removed?”. Difficult to say especially because of this linewithAppProvider()(Button)
.withAppProvider
is called and the return value is also called. Are there any side effects when callingmerge
hoistStatics
? Are there side effects when assigningWithProvider.contextTypes
(Setter?) or when readingWrappedComponent.contextTypes
(Getter?).Terser actually tries to figure it out, but it doesn’t know for sure in many cases.
I’m not saying Terser is bad because it can’t figure it out. It’s just too difficult to determine it reliable in a dynamic language like JS. Even Rollup, which has a very advanced side effects detection isn’t able to figure it out here.
It’s actually possible to help Terser here with the
/*#__PURE__*/
annotation. It flags a statement as side effect free. So a simple change would make it possible to tree-shake the code:This would allow to remove this piece of code. But there is still the problem with the
imports
which need to be included/evaluated because they could contain side effects.To takle this problem I invented the sideEffects property in package.json.
It similar to
/*#__PURE__*/
but on module level instead of statement level. It says: “If no direct export from a module flagged with no-sideEffects
is used, the bundler can skip evaluating the module from side effects.”.In the polaris example original modules look like this:
That’s actually a bit buggy, so assume it’s like this:
For
import { Button } from "@shopify/poliaris"
this has the following implications:include it
: include the module, evaluate it and continue analysing dependenciesskip over
: don’t include it, don’t evaluate it but continue analysing dependenciesexclude it
: don’t include it, don’t evaluate it and don’t analyse dependenciescomponents/Breadcrumbs.css
even if they are flagged with sideEffects.In this case only 4 modules are included in the bundle:
After this optimization other optimizations can still apply, i. e.
buttonFrom
andbuttonsFrom
exports fromButton.js
are unused tousedExports
optimization kicks in and Terser may be able to drop some statements from the module.Module Concatenation also apply and these 4 modules plus the entry module (and probably more dependencies) can be concatenated. So
index.js
has no code generated in the end.@donifer Don’t bundle your library.
sideEffects
works on the module/file level, so bundling everything into one file means any import will require that whole file.Just run babel on your files plus postcss or whatever is needed to ensure that the css output is vanilla.
What we really nead is something like Rollup that can work on individual files. It would essentially takes an index file, walk its re-exports, and transform those individual files with module level DCE. But importantly, it wouldn’t actually bundle everything.
Please someone correct me if I’m wrong, since I’m not sure I understood.
sideEffects
will then only work if the library is not bundled into one file?If that is the case whats the recommendation for building libraries nowdays? Rollup helps with some code elimination when bundling but it generates a bundle which won’t work with
sideEffects
. I feel pretty lost.Rollup has a preserveModules setting that keeps individual files instead of bundling.
Can we reopen this issue? I believe there is a lot of confusion on how to make tree shaking work properly. I’d be happy to share some repro projects or more insights about the main issue
Hi @sokra thanks for having a look at this.
So side-effects concerns the library webpack is building and in my case side-effects is referring to unused exports in my package, and not unused modules in
@shopify/polaris
? I think I have a misunderstanding here.The way I thought tree shaking worked is as long as the library I’m depending on used ES6 module exports, webpack would remove unused code brought in from said module.
So in this case,
@shopify/polaris
is exporting components of the library inindex.es.js
as such:(abbreviated index.es.js):
In my module, if I do the following:
Then
ActionList
would be pruned as tree shaking would remove the code since it’s not referenced.But this isn’t the case. Because the components are exported as a default export, so in this case I bring in the entire library because I import the default export containing all components.
Had the components been exported as such:
Then
ActionList
would have been pruned?I’m confident this issue remains.
@sokra I think it would be useful to mention @brandonkal 's point in the Tree Shaking documentation. I wasted several hours until I found this.