webpack: Tree shaking completely broken?

updated with the latest versions (29.06.2017) Webpack - 3.0.0 Babel-preset-env - 1.5.2 Node - 8.1.2 OS X - 10.12.5

Current

Tree shaking is not removing imports, not used in the app.

Expected

Tree shaking, removing not used imports.


The test case is a simple app with React & React Router v4. Link component is not used, but the whole router package is bundled.

import React from 'react'
import ReactDOM from 'react-dom'
import { Link } from 'react-router-dom'

ReactDOM.render(
  <p>Hey</p>,
  document.getElementById('app')
)

Webpack & Rollup test repo

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 62
  • Comments: 163 (21 by maintainers)

Commits related to this issue

Most upvoted comments

Tree shaking should occur independently from minification/dead code removal IMO if possible. Otherwise it really doesn’t rely on Webpack, it relies on a different 3rd party tool.

For example, when I run Rollup to produce a bundle, it’s tree shaken without any sort of minification or dead code removal.

This is important for the Ionic Framework team because in order to support Webpack, Closure Compiler and Rollup independently, we run minification/dead code removal as a separate step independent from the bundling process.

Thanks, Dan

Thank you everyone for keeping this issue up to date. I have to apologize for not communicating the update on how we plan to tackle this issue.

So after we have finished our 2.3 regression milestone for bugfixes for webpack 2(.2) final, we start on features.

Therefore I’m going to add this to our feature milestone.

So tl;dr https://github.com/mishoo/UglifyJS2/issues/1261 is the biggest hurdle on solving this problem. So we either have to:

See if implement scope hoisting (rollup features) or build a new general purpose optimizer that supports a level of program flow analysis.

We would really appreciate as much brainstorming as possible to how we could solve this.

I’ve hit the same problem:

index.js

import {sudoMakeMeASandwich} from './re-export';
console.log(sudoMakeMeASandwich); // make sure this one is used.

re-export.js

export {makeMeASandwich, sudoMakeMeASandwich} from './helpers';
export {unusedHelper} from './unused-helper';

helpers.js

export const makeMeASandwich = () => 'make sandwich: operation not permitted';
export const sudoMakeMeASandwich = () => 'one open faced club sandwich coming right up';

unused-helper.js

import * as unused2 from './unused-helper-2';
export const unusedHelper = unused2;

unused-helper-2.js

export const FOO = 'FOO';

Expected output

The expected output is a bundle with only the following modules: index, re-export and helpers. The unused modules should not be included, as those are only re-exported by re-export.js, but this export is never used.

Actual output

/******/ (function(modules) { // webpackBootstrap

/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

"use strict";
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__helpers__ = __webpack_require__(1);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__unused_helper__ = __webpack_require__(3);
/* unused harmony reexport makeMeASandwich */
/* harmony reexport (binding) */ __webpack_require__.d(exports, "a", function() { return __WEBPACK_IMPORTED_MODULE_0__helpers__["a"]; });
/* unused harmony reexport unusedHelper */



/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {

"use strict";
/* unused harmony export makeMeASandwich */
/* harmony export (binding) */ __webpack_require__.d(exports, "a", function() { return sudoMakeMeASandwich; });
var makeMeASandwich = function makeMeASandwich() {
  return 'make sandwich: operation not permitted';
};
var sudoMakeMeASandwich = function sudoMakeMeASandwich() {
  return 'one open faced club sandwich coming right up';
};

/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(exports, "FOO", function() { return FOO; });
var FOO = 'FOO';

/***/ },
/* 3 */
/***/ function(module, exports, __webpack_require__) {

"use strict";
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__unused_helper_2__ = __webpack_require__(2);
/* unused harmony export unusedHelper */

var unusedHelper = __WEBPACK_IMPORTED_MODULE_0__unused_helper_2__;

/***/ },
/* 4 */
/***/ function(module, exports, __webpack_require__) {

"use strict";
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__re_export__ = __webpack_require__(0);

console.log(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__re_export__["a" /* sudoMakeMeASandwich */])());

/***/ }
/******/ ]);

Rollup

As a small comparison, rollup outputs only:

const sudoMakeMeASandwich = () => 'one open faced club sandwich coming right up';
console.log(sudoMakeMeASandwich());

Any update on this? I am surprised to see this not a part of the 2.x milestones. This is a major issue IMO because this is one of the “killer apps” of Webpack 2.x and it doesn’t seem to be working as one would expect for even basic examples.

What can we do to help expedite this?

Thanks, Dan

Because the whole internet is full of articles like “BREAKING NEWS: WEBPACK 2 BRINGS ES6 TREE SHAKING. FIND OUT MORE IN THIS ARTICLE” most of which were written long before Webpack 2 was released and it became obvious that Webpack 2 itself doesn’t actually tree-shake.

You mean articles like this one in the official docs?

Since webpack is 2.6 now and tree-shaking is still not working, is there any news on this? @sokra

@Rich-Harris Thanks for chiming in here, it’s a huge help to hear your perspective.

Rollup used to do the ‘if it’s an empty import, assume it might have side-effects, otherwise assume it doesn’t thing’ — it didn’t work.

Is there any way to turn this back on? If not, I’d be curious to know the last version of Rollup that does this.

Code in the wild breaks those expectations.

Would it be possible to add a “blacklist” of non-tree-shakeable modules to the bundler config (as suggested above by @arackaf)? Also, it would be super useful to have some examples of modules in the wild that break this expectation.

@arackaf 👍 for the clearly expressed idea:

I hope something like a new, opt-in flag could be added, telling Webpack everything is pure, and tree-shaking eligible. Or maybe some sort of new tree shaking config option where the developer opts into tree shaking by providing info on which, if any modules are impure, and therefore cannot be shaken out.

Here’s a straw man proposal for this might look in the Rollup / Webpack config:

{
  treeShaking: {
    assumePure: true,
    impureModules: [ ...blacklist goes here... ]
  }
}

Summarizing the expected semantics from earlier discussions, this configuration would specify:

  • assumePure: true Assume that all modules are pure (without side effects) EXCEPT modules that are imported as “obviously with side effects”, e.g. import "d3-transition"; or import 'rxjs/add/operator/catch';.
  • impurePackages:[ ... ] Specify a list of modules that expose named or default exports AND have side effects. These modules would need to be 100% included in the resulting bundle if they are imported by any module that has any exports that are imported into the root module for the bundle, conforming to the current inclusion behavior of Rollup (and perhaps Webpack too at the moment), “erring on the side of caution”.

Here is my small investigation. Everything was tested with the same config as above:

  • Webpack - 2.1.0-beta.20
  • Babel es2015 preset - 6.13.2
  • OS X 10.11

Simple react app:

import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
  <p>Hey</p>,
  document.getElementById('app')
)

The production build is 146 kb.

Time for tests:

  1. Lets add some simple functions like this:
function fA() {
  console.log('I am a string')
}

We import some of them, but use only one:

import React from 'react'
import ReactDOM from 'react-dom'
import { fA, fB, fC, fD, fE, fF, fG } from './func'

fA()

ReactDOM.render(
  <p>Hey</p>,
  document.getElementById('app')
)

The size is still 146 kb as the function is pretty small. If we use all of them - the size is 150 kb.

Tree shaking is working.

  1. Lets make the same test with react stuff. Simple components:
import React from 'react'

const A = () => (
  <div>
    <p>Hey, I am a message</p>
  </div>
)

Lets use one component:

import React from 'react'
import ReactDOM from 'react-dom'
import { A, B, C, D, E, F, G } from './comp'

ReactDOM.render(
  <div>
    <A />
  </div>,
  document.getElementById('app')
)

The size is now 147 kb. With all components - 152 kb.

Tree shaking is working.

  1. Now we reexport the components. First, we create a file with named exports:
export { A, B, C, D, E, F, G } from './comp'

The same example as above with one component still gives us 147 kb.

Tree shaking is working.

  1. Same as previous, but with a *
export * from './comp'

Now the size with one component is 152 kb.

Tree shaking is not working (exports with *). Issue is here.

  1. Final test. We make two simple components and use them (as we know, tree shaking is working).
import React from 'react'
import ReactDOM from 'react-dom'
import { A } from './compA'
import { B } from './compB'

ReactDOM.render(
  <div>
    <A />
    <B />
  </div>,
  document.getElementById('app')
)

The size with one component - 147 kb, with both - 149 kb. Now, we take random library and import it in one of the components (lets take B):

import React from 'react'
import Select from 'react-select'

const B = () => (
  <div>
    <p>Hey, I am a message</p>
  </div>
)

We want to use only one component:

import React from 'react'
import ReactDOM from 'react-dom'
import { A } from './compA'
import { B } from './compB'

ReactDOM.render(
  <div>
    <A />
  </div>,
  document.getElementById('app')
)

Unfortunately, now the size is 185 kb.


So, the tree shaking actually removes the component, but some of the imports are included in the build (even if they were not used). Same thing happens with combined / reexported / nested files.

Any ideas why this happens with only some of the components and how to solve this?

I’m commenting with one thing only:

There have been multiple requests to the core team to answer one question: is this problem being worked on? After a very long silence we got some team members … answer different questions, but not this one.

This issue and #4784 mean one thing: tree-shaking does not work in Webpack despite all the articles and guides that claim otherwise.

There have been multiple examples showing it doesn’t work. There’s even a repo testing tree shaking set up by one of the participants of the thread. The result?

Yes let’s keep the thread on topic.

99% of the thread is on topic. What is the status of this bug?

We just came across this issue today too.

If it helps anyone else, we found a Babel plugin that provides a useful workaround in the meantime: https://www.npmjs.com/package/babel-plugin-transform-imports

As per the readme, it’s completely configurable, and can transform imports like this:

import { Row, Grid as MyGrid } from 'react-bootstrap';
import { merge } from 'lodash';

into this:

import Row from 'react-bootstrap/lib/Row';
import MyGrid from 'react-bootstrap/lib/Grid';
import merge from 'lodash/merge';

Note that this seems like it will only work if the package’s folder structure is consistent across named exports.

@jhabdas Your entire post is incorrect.

Dynamic import is not a WHATWG proposal, it is a proposal for TC39 (AKA ECMAScript, AKA JavaScript). Therefore it applies to all JavaScript targets, not just the browser.

And the proposal for ES6 modules in Node specifically mentions that dynamic import will be supported (and in fact it’s the only way to load an ES6 module from inside of a CommonJS module).

Also, dead code elimination does help with servers. Having larger JavaScript code means that Node takes longer to parse and execute the code. If you’re writing a microservice which creates/destroys servers dynamically on-demand, startup time is quite important!

Also, require is so slow in Node that some developers even bundle their server code to avoid the cost of require

Dead code elimination has nothing to do with “being asynchronous”, it has to do with removing useless code, which means smaller file sizes, faster parsing, and faster execution. That’s useful in any situation.

Most compiled languages (e.g. C, C++, Java, Haxe, OCaml, Haskell, etc.) do dead code elimination by default, even though they’re used on the server. It’s a basic language feature, not a browser feature.

Dead code elimination cannot be a runtime feature in JavaScript, because when you use import { foo } from "./foo.js" the browser has to download the entire foo.js file, and afterwards it can then apply the dead code elimination.

But applying the dead code elimination after downloading foo.js is useless, because it’s already downloaded the full file!

The benefit of dead code elimination is that the browser doesn’t have to download the full foo.js file, it only needs to download the parts that it needs. So dead code elimination cannot be done by browsers, it must be done by tools (like Webpack, Rollup, Closure, etc.)

If you want to “just build apps” then use a pre-existing template like create-react-app. Or just use plain old JavaScript. Nobody is forcing you to use these tools. The complexity isn’t going to go away, because some developers actually do need these features.

A few random thoughts on annotations from me, since it’s cropped up in a few places (here and in related issue threads):

  • Rollup used to do the ‘if it’s an empty import, assume it might have side-effects, otherwise assume it doesn’t thing’ — it didn’t work. Code in the wild breaks those expectations. I’d caution against going down that road
  • The most important thing isn’t reducing bundle size, it’s ensuring that programs are correct. If that means erring on the side of caution, unfortunately that’s what we have to do. That means annotating modules as pure, not marking them as impure. You should never need to use annotations to get correct behaviour, only to get better tree-shaking
  • Tree-shaking can (and hopefully will!) improve (on the Rollup side, I’ve been working on better tree-shaking on a feature branch… maybe I’ll get round to merging it one of these days. On the webpack side, maybe we could leverage Closure Compiler, which has been doing this sort of thing very well for a long time?). Most cases of side-effect detection false positives aren’t like the halting problem (though some might be. genuinely not sure!)
  • Annotations on the exporter side are probably more logical than on the importer side

I certainly don’t view annotations as a complete or even good solution, and haven’t done any investigation into how they’d actually work in either Rollup or webpack. (Side-note, I really want to capitalise webpack but Sean yells at me when I do.) Long-term, the solution is

  • Library authors can write code that takes full advantage of ES module semantics. There’s probably room for some guides and examples on what it actually means to write tree-shakeable code
  • Libraries should distribute ES module bundles as well as UMD/CommonJS ones — webpack and Rollup both recognise the "module" field in package.json files for this. Here’s a primer for anyone unfamiliar
  • As users of libraries, you can help modernise the ecosystem by sending pull requests and helping library authors understand why this stuff is important!
  • Rollup and webpack can probably get smarter about tree-shaking, as discussed

Hi Appreciate all your hard work and my sincere thank you to who ever maintains Webpack. I’ve got a question , Do you guys have an idea of how far you’re away from releasing this fix or is it gonna be fixed at all or not ? We have a massive dependency on Webpack in all of our projects and looking at how big our bundles are is just horrifying, We might decide to move to rollup, but then we need to find a way to integrate rollup with Angular-Cli. Could you please give us a rough estimation on how long do you guys think we should wait possibly ? Much much appreciation again. Cheers

Webpack does not really do Tree Shaking. …it won’t be as effective as Rollup Tree Shaking.

Right. I think these are the main issues here. Folks want Webpack to really do Tree Shaking, like Rollup does, and it doesn’t. As I understand, it’s a feature on the Webpack roadmap that has not yet been implemented.

I believe what’s blocking implementation of proper tree-shaking (relying only on ES6 imports/exports, before passing the bundle to UglifyJS) is the fact that detecting whether a module has side effects may be undecidable like the Halting Problem.

One practical solution would be to allow authors to annotate their definitions as “tree shakable” (so-called “pure” definitions with no externally relevant side effects) or “non-tree shakeable” (so-called “impure” modules, that do have externally relevant side effects, e.g. d3-transition which mutates the prototype of another module). This would allow Webpack to bypass the complex process that Rollup implements; programmatically (and speculatively) deciding whether or not a module has side effects.

There are several threads related to this topic:

The syntax proposed by @sokra and implemented in UglifyJS is the following:

/*#__PURE__*/import { something } from "module";
//#__PURE__ importing my magic module
import { something } from "module2";

There’s some debate as to whether the annotations should go on the exports or the imports.

But conceptually, Webpack could leverage these annotations when walking the ES6 imports/exports tree before passing the bundle into UglifyJS, in order to achieve tree-shaking at the module level. This would be opt-in by library authors (e.g. D3), but at least it would take us closer to tree-shaking-nirvana than we are today.

@danbucholtz Tree shaking is the same thing as dead code elimination, because according to the ECMAScript spec modules must be evaluated even if their exports aren’t used.

Therefore if you use import { foo } from "./foo" Rollup will include the "foo.js" module even if you never use the foo variable. Rollup uses dead code elimination to remove code which isn’t used.

However, I agree with you that it should be handled properly by Webpack rather than relying upon another tool like UglifyJS.

Rather than Webpack duplicating Rollup’s efforts, I think it would be cool if there was a JS tool which could take ES6 modules, run tree shaking/dead code elimination on them, and then return tree shaken ES6 modules. That tool could be used by both Rollup and Webpack.

Tree shaking still not working as expected with rc.3

@trainiac yup, the problem has been documented here a couple of times already. This issue is now being geared more towards solving it. As they’ve said they’re going to be releasing this in the 2.4 feature milestone.

I know you’re eager to see this fixed, as it’s the reason many teams (including mine) upgraded to Webpack 2.0. We feel your pain.

Rest assured, it’s being worked on (and the core team probably wants it fixed more than the users do).

Are my expectations of what tree shaking can do too high? Do we have to wait for libraries to stop distributing compiled versions?

Exactly. Most libraries still ship with CommonJS that’s why it will take some time until tree-shaking will be actual effective.

Webpack will destroy itself without Tree Shaking. Most of the people who are using it don’t realise there are lots of unused code and big modules in their bundle, but as soon as they start working on optimisation, which every one knows it’s always 2 days before releasing your application 😃 , they realise shit has hit the fan terribly 😃

Looking at frameworks like Angular4, one of their selling point is “Tree Shaking” because the bundle is huge, but they never say it’s broken 😃

They should start using something else in Angular CLI

@xe4me up until a feature we are adding, webpack has always been treeshaking correctly based upon the esm module specification. But we are going to add an optin feature that package creators must implement. See https://github.com/webpack/webpack/tree/feature/pure-module/examples/pure-module

hi, all!

it’s working using bable/minify . here is the repo: babel-webpack-tree-shaking 😄

maybe we need someone to summarize the discussion. 😃

@arackaf I think that’s a great idea - to hint for the existence of side effects, rather than their non-existence, as this modules with side effects are less common than pure ones. Of the 31 modules exported from d3’s index.js, only one of them, d3-transition has side effects.

@thelgevold That’s also a fantastic idea - to have import statements without “imported” symbols be the hint for side effects (for non-tree-shakeability). This would also work in the case of d3-transition, as that’s how it’s imported as well: import "d3-transition";.

These two ideas seem sound, non-intrusive, and implementable.

is the fact that detecting whether a module has side effects may be undecidable like the Halting Problem.

Indeed, but I hope this would be an opt-out, rather than opt-in workaround. In other words, I hope webpack wouldn’t compel authors to mark the 99% or so of their modules which do not cause side effects.

Ideally, I’d hope developers would be responsible for marking those modules which can NOT be tree shaken, no matter what. I know this might lead to some subtle bugs, as devs fail to do this and code is not run in production, but that could be managed in any number of ways: perhaps have developers opt into tree shaking with a new flag, and clearly list out which modules were shaken out, with a note about how to prevent this.

For those who follow this issue, or are subscribed to it, please note that it is now documented as “Caveats” in main documentation: https://webpack.js.org/guides/tree-shaking/#caveats

I’m trying to understand this. So Babili requires Babel, which started to cover gaps in a shifting spec TC39 before becoming plugin-based in and is being used in a plugin-based bundler created to build SPAs which aren’t crawable to most search engines and builds with a platform which doesn’t support ES Modules currently. Does that sound right? I must be missing something.

Let me address your sarcastic remarks.

  1. Babili is a JavaScript minifier. Minifiers use JavaScript parsers to do the minification. Babili uses Babel to parse JavaScript
  2. Babel is a toolchain for JavaScript parsing and transformation.
  3. Webpack is a JavaScript bundler. Bundler transforms assets to optimize initialization in various browsers.
  4. ES modules is an incomplete feature of JavaScript. JavaScript toolchains made various incompatible workarounds to make it usable today.
  5. Search engine optimization has nothing to do with Webpack. Both SPA are adapted to be consumable by search engines using server side rendering and search engines are adapted to SPAs using headless browsers.

Overall this sounds like a trolling off-topic.

I’ve been subscribed to this thread for a while. Seeing numerous attempts to find a suitable way of reporting a failing test case ranging from copy-pasting file output here in comments to putting up various sample repos on github. I thought that a standardized way of reporting such ES6 tree shaking bugs would be more appropriate. I’ve made up a utility repo for conveniently testing ES6 tree shaking and reporting bugs. https://github.com/halt-hammerzeit/es6-tree-shaking-test Bug reporters can fork that repo and add their own test cases posting links to their forks here for quicker response.

Also, I most definitely do not want to have to write things like /*#__PURE__*/ in my code. For the most common cases, this should “just work”.

I am also thinking that modules with side effects don’t need to actually export something.

Can’t we just import those modules using imports without “imported” symbols. Take an example from rxjs:

import 'rxjs/add/operator/catch';

Behind this import with no exported symbol we have the following module with side effect(s):

import { Observable } from '../../Observable';
import { _catch } from '../../operator/catch';
Observable.prototype.catch = _catch;
Observable.prototype._catch = _catch;

Then for “pure” modules without side effects we can just do: import {MyToken} from './whatever'

Just through encouraging “pure” modules, we can probably reap most of the benefit of Tree Shaking through a convention where we limit side effects to small, isolated modules.

In case another minimal test case for tree shaking would be useful, here are two identical examples that create a custom bundle for D3.js, one with Rollup and one with Webpack:

One definition that exemplifies the difference is Local - this appears in the Webpack bundle, but not in the Rollup bundle. It is exported from d3-selection, but not imported anywhere, and has no side effects, so should not be included after tree shaking.

Confirmed that the ES6 source (specified by jsnext:main and module in package.json) is being used by Webpack from the packages d3-selection and d3-transition, not the CommonJS builds (search the bundle for console.log("This is a custom change to the ES6 source only for confirming that Webpack is using the ES6 module source, not the CommonJS build"))

Based on this experiment, I can confirm that WebPack (as of version 2.2.1) does not perform tree shaking as expected (at least for the case of these D3 modules).

For context, I ended up here while researching WebPack as an alternative to Rollup, following up on a conversation with @TheLarkInn and @Rich-Harris on potentially introducing annotations to flag ES6 modules as “pure” (free from side effects and thus tree-shakeable) from the D3 issue Rollup not tree-shaking from “d3” package #3076. Flagging exports as “pure” by adding hints in comments may be a potential path forward for making tree shaking work reliably. At least it would give library authors a way to easily make their libraries tree-shakeable.

dont use import as it bundles the code to one file

use import().then in all code. so it will be downloaded on demand.

you will see the bug here…

On Aug 10, 2017 6:36 AM, “Paul Sachs” notifications@github.com wrote:

So here’s a fun example with code splitting:

// index.js export function C() { import(‘./A’).then(module => { console.log(module); });

import(‘./B’).then(module => { console.log(module); }); } // A.js import { A } from ‘./lib’; export default function () { console.log(A()); } // B.js import { B } from ‘./lib’; export default function () { console.log(B()); } // lib.js export function A() { return ‘AAAAAA’; } export function B() { return ‘BBBBB’; }

The results show that both bundles (A and B) contain both functions from lib despite only using a single function each. fuction A is dead in chunk B, function B is dead in chunk A.

I thought webpack would be marking dead code on a per chunk basis but it doesn’t look like it.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/webpack/webpack/issues/2867#issuecomment-321400214, or mute the thread https://github.com/notifications/unsubscribe-auth/AZMy4Us4p8NNdrtyM8PilA4JqYF9CDT2ks5sWjSDgaJpZM4JjvHI .

Well this hurts me feels. I’ve been trying to get tree shaking working with webpack for many many many hours, At least now i know why it does nothing 💃

I’m always a fan of developers being responsible for knowing their tools, and how to use them, and being responsible when things break as a result of them not accomplishing this basic duty.

But, if we absolutely have to have safe defaults, I hope something like a new, opt-in flag could be added, telling Webpack everything is pure, and tree-shaking eligible. Or maybe some sort of new tree shaking config option where the developer opts into tree shaking by providing info on which, if any modules are impure, and therefore cannot be shaken out.

But what I hope we can all agree on is that manually specifying annotations for the potentially hundreds of modules which are eligible for shaking is not really feasible in practice.

mishoo/UglifyJS2#1261 appears to be solved by mishoo/UglifyJS2#1448. Where does that leave this issue?

I believe I’m having the same issue - tree shaking doesn’t seem to be doing much of anything. The project is https://github.com/redmountainmakers/kilntroller-ui. I haven’t narrowed it down to a specific bug but here is an example of a dependency I am definitely not using anywhere:

(image from the excellent [Webpack Visualizer](https://chrisbateman.github.io/webpack-visualizer/) tool)

You could also use babel-plugin-lodash to automatically transform this

import { throttle } from "lodash";

to something equivalent to

import throttle from "lodash/throttle";

.mjs is concretely happening. Having talks about official statement.

@curran There is such a repo https://github.com/halt-hammerzeit/es6-tree-shaking-test You can Push your test cases there and update the table in the README. “simple” test case is tree-shakeable for both of them.

Tree shaking is a great optimization technique, don’t get me wrong.

However in the context of Angular, the importance of Tree shaking is somewhat exaggerated. Mainly because a few patterns in Angular are making code less “shakable”.

In short it’s due to symbol registration in NgModules. Registering a symbol in NgModule arrays makes the code behind the symbol unshakable since Tree shakers can’t distinguish this from actually using the symbol to instantiate something in your application code.

This pattern will give you the same issue even if you use Rollup.

I have more info about some of these pitfalls in Angular here: http://www.syntaxsuccess.com/viewarticle/pitfalls-in-angular-bundling

Also, if you are interested, I have a comparison between the different bundlers here: http://www.syntaxsuccess.com/viewarticle/angular-production-builds

As you can see in my example, Rollup with Tree shaking is only 4k smaller than a regular Webpack 2 bundle. If you switch to Webpack 3, the difference is only 2k. The reduction is due to less overhead from function wrappers in Webpack 3.

Of course these results will vary from app to app, but this is from a medium size Angular application, so it’s at least an interesting datapoint.

Angular CLI is currently using Webpack, but who knows, they might switch to something different in the future.

In any event, we shouldn’t exaggerate the importance of Tree shaking in Angular. All the extra benefits added from Webpack in general (e.g. lazy loading) more than weigh up for relatively minor increase in bundle size.

Angular experiments with /*@__PURE__*/: https://github.com/angular/tsickle/issues/452

So, in what scenarios does tree-shaking currently actually work? Or does it…I’m confused.

No, webpack 2 should be able to do tree shaking with namespace imports, as long as the namespace export doesn’t escape.

The important part is that the imported file must use ES6 exports.

On Fri, 2 Sep 2016 at 0:47 jonathan schatz notifications@github.com wrote:

i think webpack2 needs that to be: ‘’’ import {Vector3} from “three/src/Three.js” ‘’’

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/webpack/webpack/issues/2867#issuecomment-244122558, or mute the thread https://github.com/notifications/unsubscribe-auth/AAEdfaHY5722UN-TDX1OJCwnHQS4yFQqks5qlvOZgaJpZM4JjvHI .

you could try it again with webpack beta 21, as I fixed an issue with reexports (exports * from '...')

https://github.com/sokra/react-rollup-webpack-test

Note that instead of adding the sideEffects flag to the config, it could also be added to the package.json of react-router-dom.

webpack 4 allows you to opt-in into more aggressive tree shaking.

It seems that if you have dead code that is removed via a DefinePlugin the dependencies won’t shake out. For instance:

import { DevOnly } from './dev-only';
if (process.env.NODE_ENV !== 'production') {
  DevOnly();
}

I noticed that the code for DevOnly would still end up in the client bundle. I was able to get around this by using transform-define at the loader level instead of DefinePlugin at the plugin level. Here is an example of what this looks like:

const webpack = require('webpack');
const BabiliPlugin = require('babili-webpack-plugin');
const ShakePlugin = require('webpack-common-shake').Plugin;

module.exports = {
  entry: ...,
  output: ...,
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        query: {
          cacheDirectory: '.babel_cache',
          babelrc: false,
          plugins: [
            [ 'transform-define', { 'process.env.NODE_ENV': 'production' } ],
          ],
          presets: [ 'babili', 'react' ],
        },
      },
    ],
  },
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin,
    new ShakePlugin,
    new BabiliPlugin,
  ],
};

@VladShcherbin I don’t think it’s fair to say that Tree shaking “doesn’t work” in general as an optimization technique. (If you are using Rollup).

However, you have to write your code a certain way to benefit from it. If you are using a framework, some of the code structures choices have been made for you. Unfortunately, in some cases these choices will impact the effectiveness of tree shaking.

Tree shaking is awesome, but it’s a much more “primitive” symbol discovery technique than people might think. There are well known limitations of this approach.

The most important limitation is that tree shaking happens at the statement level (e.g. class).

In the case of a class, Tree shaking will not “shake out” unused methods. Nor will it shake out modules imported by unused methods.

However, if you are aware of these limitations, you can definitely benefit from Tree shaking in Rollup by structuring your code accordingly.

You can of course give the Closure compiler a try if you need optimizations at a much more granular level.

@thelgevold tree shaking is a great idea, but it’s not working in Webpack and in Rollup. No matter how you write your es6/7/10 modules, it still won’t work in even simplest cases. But do you remember the hype it get, all the tutorials and other fairy tales about it?

In fact, if it was working, your bundle would be probably reduced by 2 or 3 times that you probably won’t even need lazy loading.

And here we now, a year later waiting for any member/maintainer to give us some info, is it going to work someday, is someone trying to make it better.

Exactly. Couldn’t have said it better myself thanks Tor

@xe4me: “They should start using something else in Angular CLI”… what would you suggest?

@cruzlauroiii The official tree-shaking guide has no info about babili. If using it solves the issue, has no side-effects and is the recommended way, it should be probably mentioned in the docs.

It would be great to hear some info on this from the core team. @sokra @TheLarkInn

+1 that assuming named imports are side-effect-free is a really nice intuitive solution. If a named import should have side effects:

  • The imported module probably shouldn’t be written this way in the first place 😃
  • Calling code could hint this as follows:
import 'my-sortof-broken-module';
import someFunction from 'my-sortof-broken-module';

@thelgevold exactly my idea, too. I think this would be a good default convention: named exports/imports are pure, import the file for side effects like polyfills. If this would have an explicit opt-out for (some?) lib authors who want named exports/imports with side effects, I think this would be a good solution for now…?

@curran Webpack does not really do Tree Shaking. The issue is that Webpack can’t remove code at the statement level. Example: If you have two exports in your module, both exports will be included in your final bundle. (e.g. two classes) I have an experiment here where I show this: http://www.syntaxsuccess.com/viewarticle/tree-shaking-with-webpack

Instead of Tree Shaking, Webpack defers to Uglify for code removal. This means it will be able to remove stuff like unused variables etc, but it won’t be as effective as Rollup Tree Shaking.

there’s also this one (which despite the name supports generic usage):

https://github.com/lodash/babel-plugin-lodash

{
  plugins: [
    'lodash', 
    ['lodash', { id: 'react-bootstrap' }] 
  ]
}

i’ve used it like this for lodash and react-bootstrap without any issues for quite a while now but i’m curious to try out the transform-imports plugin to compare.

FYI: I created another minimal test case for tree shaking with webpack 2.

For me tree shaking works as long as the return value of an import is not used inside another function - even if this function is never imported itself. E.g. if you take this lib logBaz can be tree shaked, because it is just re-exported. logFizz and logBuzz can be tree shaked, because they are a first level export. But: Sadly foo or doBar can never be tree shaked even though logFoo and logBar are never imported.

There we go, lot of lines like this one

WARN: Side effects in initialization of unused variable replaceLocation [0:24087,4]

These variables remain in the minified version.

Documentation is in progress. It’s still in alpha…

I’m experiencing a similar issue with react-router-dom. Modules such as MemoryRouter and StaticRouter shouldn’t be included in the bundle if using named exports. Tested on a fresh create-react-app installation with react-router-dom and the following App.js:

import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const About = () => (
  <div>
    <h2>About</h2>
  </div>
);

const BasicExample = () => (
  <Router>
    <div>
      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
    </div>
  </Router>
);

export default BasicExample;
screen shot 2017-11-02 at 17 55 25

This is not a fix for #4784 but this may help someone… I’ve created the following gist that explains how to compile npm packages using babel; and ensure that the application using these npm packages can benefit from tree shaking. https://gist.github.com/ncochard/6cce17272a069fdb4ac92569d85508f4 This illustrates the use of webpack’s mainfields and babel’s babel-preset-env. Feedback welcomed!

So, I did some experiments over the last couple of days cause Tree Shaking didn’t seem to work on our Angular projects (using Angular CLI for the build chain).

Seems to me that Webpack / UglifyJS is only able to do tree shaking on the “first level” (not recursively). For instance, when importing dependencies into my AppModule (the root Angular module), tree shaking works just fine. However, when importing dependencies into some other Angular module which is then imported into the AppModule, tree shaking fails - and everything (!) gets imported.

Not sure if this is only related to Angular projects, or whether it’s an issue with the Angular CLI / Webpack / UglifyJS …

Yes let’s keep the thread on topic. However any usability concerns can go into a separate issue for discussion! 🤗

It would be interesting to have a kind of “Rosetta stone” of small code samples with patterns that are known to be tree-shakeable and not-tree-shakeable, via Rollup vs. Webpack. Does anyone know of any resource like this?

It works fine by balili with local module, but it is broken with module in node_modules like lodash-es

I did an experiment with Webpack and Babili . Since it works with ES6, the dead code removal results are better.

Exported classes are not included unless they are actually imported. http://www.syntaxsuccess.com/viewarticle/webpack-tree-shaking-with-babili

this is a working tree shaking in webpack babili

https://github.com/blacksonic/babel-webpack-tree-shaking

Yeah, this is the article that has started it all: http://2ality.com/2015/12/webpack-tree-shaking.html

Marking modules as unused along with uglify processing was named “tree shaking”. But it isn’t as was explained by rollup’s author @Rich-Harris https://medium.com/@Rich_Harris/tree-shaking-versus-dead-code-elimination-d3765df85c80 Or is it?

I think we need a Wikipedia article about tree shaking. https://en.wikipedia.org/wiki/Tree_shaking

@markdalgleish that’s a fantastic workaround, thanks for sharing!

I did a small investigation a while ago. Based on a couple of issue threads and HackerNews comments, I think that proper tree-shaking (rollup way) will be implemented after v2 is released. There is also an open issue in UglifyJS which is preventing good tree-shaking results right now. https://github.com/mishoo/UglifyJS2/issues/1261

Adding another datapoint: Tree shaking seems ineffective in real-world projects.

I have a vendor.js chunk with a bunch of libraries (React, ReactDOM, MobX, etc.). Using Babel 6.14.0 and Webpack 2.2.0.rc.3 the file compiles to 2.34MB without minification.

Enabling minification brings it down to 939kB.

Disabling unused and dead_code flags on UglifyJsPlugin adds 30kB. This implies tree shaking was only able to remove 4% of the library code. That does not sound like a lot.

Adding/removing the {modules: false} option on Babel does nothing. Potentially (probably) the libs are precompiled to use CommonJS.

The initial large file size leads me to believe the libraries in question are not pre-minified.

Are my expectations of what tree shaking can do too high? Do we have to wait for libraries to stop distributing compiled versions?

@andreigabreanu with such a simple example it may work, but with daily projects that use another libraries it does not.

Here is my simple test repo, super simple example with React & React Router. React Router was chosen as its creators tried to make it compatible with rollup/webpack tree-shaking, but it still does not work with the latest versions.

i think webpack2 needs that to be:

import {Vector3} from "three/src/Three.js"

This seems to be a problem only in combination with reexports…

Ran into the same issue with the latest rxjs version when importing like this: import { concat, map, merge, share, switchMap, take, toArray } from 'rxjs/operators'; instead of separate imports 😦

Yes @weisk, but if you use lodash excessively, this gets bothering pretty fast, especially since there is alternate ways like @noppa mentioned.

@bkniffler why would you need it for lodash? In lodash you can import the micromodules directly, I dont think you should import it globally

@darrenscerri Webpack doesn’t optimize named exports. While Uglify can remove unused modules sometimes, it’s not guaranteed to be removed.

For example, react-router-dom: https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/index.js

There’s no guarantee there’s no side effects when you import something. So Uglify will rather keep modules than remove them.

If there was some way module creator can tell that their module doesn’t have side effects. As it was already discussed in this thread, Uglify has some special comments specification of “pure” modules.

TL;DR: Use this:

import BrowserRouter as Router from "react-router-dom/BrowserRouter";
import Route from "react-router-dom/Route";

@dominique-mueller Well importing in the Angular sense NgModules into other NgModules is quite distinct from importing with regular imports such as import { Component} from '@angular/core';.

An entry file for Angular in Webpack is further typically not your AppModule. Should rather be a main.ts or similar which imports the AppModule and bootstraps it.

So think you are more likely talking about an Angular issue. But need a more concrete example to be sure.

That being said the angular-cli team is going through some good strides to do tree shaking using their build optimizer which is integrated into angular-cli but also available as a separate webpack plugin. It’s still under active development.

@TheLarkInn

  1. Current behavior can also effect apps, not necessarily via some package.
  2. Problem is not necessarily only limited to reexports, any import is effected.

Just want to make sure that pure-module will also solve those two cases.

For example see: https://github.com/shahata/tree-shaking/blob/master/README.md

One tip. Delete your .babelrc file in your project dir. Mine had this:

{
  "presets": ["es2017-node7"]
}

This caused the minimal example to not work at all. After this, the square function stripped off from the production bundle as it should.

However, in three.js still imported everything. Maybe related issues:

https://github.com/mrdoob/three.js/issues/10711

https://github.com/rollup/rollup/issues/1130

https://github.com/mrdoob/three.js/issues/9852

Some info from core members would be great since lots of devs are interested in this.

I saw improvements in version 3.

Not mind blowing results though… Roughly a 2k reduction in bundle size, but also slightly faster execution times.

Write up: http://www.syntaxsuccess.com/viewarticle/webpack-3-vs-webpack-2

Updating to 3.0.0 and turning on scope hoisting reduced the file size by 1kb. According to feature votes and v3 release article it seems that this issue won’t be fixed anytime soon. 😕

That’s because of so called “index.js problem”. That’s why people say “tree shaking is broken”. It never did.

import { extend } from 'lodash-es'

vs

import extend from 'lodash-es/extend'

if you see the definition of three shaking, shaking the tree so dried leaves fall off.

imports are removed if not used.

so imagine if done correctly, all the non used imports, function, methods or variables will not be included in the source code if not used in the main function executable.

ive tried uglify and babili.

only this project works with tree shaking and that is webpack babili combination

https://github.com/blacksonic/babel-webpack-tree-shaking

UnusedComp wasn’t dropped because it was referenced by the static UnusedComp.propTypes outside the class IIFE which is considered a side effect from uglify’s perspective.

I’m guessing that Babel generated UnusedComp.propTypes outside of the IIFE? If it were generated inside the class IIFE, then the unreferenced UnusedComp would have been dropped.

The code from the docs is an example of code that can be removed by uglify. If you use plain functions or variables, regular minifcation will act like a “tree shaker”.

However since we live in the age of Typescript, things get more complicated.

A Typescript class like:

export class FriendService {
  getFriend() {
    return 'Joe Smith';
  }
}

Transpiles down to something like this:

var FriendService = (function () {
    function FriendService() {
    }
    FriendService.prototype.getFriend = function () {
        return 'Joe Smith';
    };
    return FriendService;
}());

Since this is a more complicated structure, Uglify will not strip it out during bundling. That is why you see all exported classes in your bundle.

I show this in my article here: http://www.syntaxsuccess.com/viewarticle/tree-shaking-with-webpack

I know this won’t work for third party code, but my recommendation is to define only one class per module. That way you get around the tree shaking limitation in Webpack for now.

Would it be possible to integrate some of the internals from Rollup to extract the tree of fragments that should be included in the bundle? Then piece the bundle together from that? Excluding anything that isn’t in the Rollup tree.

Shouldn’t that in theory be compatible with webpack bundling if you ran the “Rollup” step as part of a preprocessing step? I apologize if I am missing something obvious, but what would be the argument against this approach?

I would love to see proper Tree Shaking support instead of relying on “after the fact” code removal techniques via Uglify.

I read about the plans to integrate Closure Compiler as well. That would likely yield even better results, but you will likely face some external lib compatibility issues in ADVANCED_MODE. I guess it could be an opt-in for code bases where you control all the code and can push for Closure compatibility.

Thanks, Tor

I would hope any reliance on UglifyJS is removed and that Webpack would either handle it directly or as https://github.com/webpack/webpack/issues/2867#issuecomment-280695858 says, a tool independent of minification, especially so things like this can move forward: https://github.com/webpack/webpack/issues/3673