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

  1. Run npm install
  2. Run npm build
  3. 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 in package.json [1] and as a module rule [2]. I also ensured optimization.sideEffects: true [3].

  • Ensure mode is production. 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)

Most upvoted comments

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:

import { Button } from "@shopify/polaris";

The prebundled version looks like this:

import hoistStatics from 'hoist-non-react-statics';

function Button(_ref) {
  ...
}

function merge() {
  var _final = {};

  for (var _len = arguments.length, objs = new Array(_len), _key = 0; _key < _len; _key++) {
    objs[_key] = arguments[_key];
  }

  for (var _i = 0, _objs = objs; _i < _objs.length; _i++) {
    var obj = _objs[_i];
    mergeRecursively(_final, obj);
  }

  return _final;
}

function withAppProvider() {
  return function addProvider(WrappedComponent) {
    var WithProvider =
    /*#__PURE__*/
    function (_React$Component) {
      ...
      return WithProvider;
    }(Component);

    WithProvider.contextTypes = WrappedComponent.contextTypes ? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes) : polarisAppProviderContextTypes;
    var FinalComponent = hoistStatics(WithProvider, WrappedComponent);
    return FinalComponent;
  };
}

var Button$1 = withAppProvider()(Button);

export { ..., Button$1 };

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 line withAppProvider()(Button). withAppProvider is called and the return value is also called. Are there any side effects when calling merge hoistStatics? Are there side effects when assigning WithProvider.contextTypes (Setter?) or when reading WrappedComponent.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:

var Button$1 = /*#__PURE__*/ withAppProvider()(Button);

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:

// index.js
import './configure';
export * from './types';
export * from './components';
// components/index.js
...
export { default as Breadcrumbs } from './Breadcrumbs';
export { default as Button, buttonFrom, buttonsFrom, } from './Button';
export { default as ButtonGroup } from './ButtonGroup';
...
// package.json
  "sideEffects": [
    "**/*.css",
    "**/*.scss",
    "**/configure.js"
  ],

That’s actually a bit buggy, so assume it’s like this:

// package.json
  "sideEffects": [
    "**/*.css",
    "**/*.scss",
    "./esnext/index.js",
    "./esnext/configure.js"
  ],

For import { Button } from "@shopify/poliaris" this has the following implications:

include it: include the module, evaluate it and continue analysing dependencies skip over: don’t include it, don’t evaluate it but continue analysing dependencies exclude it: don’t include it, don’t evaluate it and don’t analyse dependencies

  • index.js: No direct export is used, but flagged with sideEffects -> include it
  • configure.js: No export is used, but flagged with sideEffects -> include it
  • types/index.js: No export is used, not flagged with sideEffects -> exclude it
  • components/index.js: No direct export is used, not flagged with sideEffects, but reexported exports are used -> skip over
  • components/Breadcrumbs.js: No export is used, not flagged with sideEffects -> exclude it
    • This also excluded all dependencies like components/Breadcrumbs.css even if they are flagged with sideEffects.
  • components/Button.js: Direct export is used, not flagged with sideEffects -> include it
  • components/Button.css: No export is used, but flagged with sideEffects -> include it

In this case only 4 modules are included in the bundle:

  • index.js (pretty much empty)
  • configure.js
  • components/Button.js
  • components/Button.css

After this optimization other optimizations can still apply, i. e. buttonFrom and buttonsFrom exports from Button.js are unused to usedExports 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.

What we really nead is something like Rollup that can work on individual files.

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.

Side-Effects works by skipping modules/files when no export is used. Since the whole package is actually only a single file (prebundled) this doesn’t work.

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 in index.es.js as such:

(abbreviated index.es.js):

function ActionList(_ref) {
   // code
 }

function TextContainer(_ref) {
   // code
 }

export {
  ActionList,
  TextContainer
}

In my module, if I do the following:

import TextContainer from '@shopify/polaris'

function doSomethingWithTextContainer() {
  // something with TextContainer
}

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:

export ActionList;
export TextContainer;

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.