question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Tree-shaking improvements

See original GitHub issue

The issue came up on Twitter - https://twitter.com/brian_d_vaughn/status/936721354283294720 and it’s also discussed in the main README. I believe we can provide a better developer experience out of the box at slight costs.

What’s going on with a library such as react-virtualized? Tree-shaking algorithms are extremely fragile (especially webpack’s - rollup’s is way superior at the moment) and they bail out easily from the optimizations.

Why? It is hard to statically analyze a language, even harder analyzing dynamic language and those algorithms don’t want to break anyone’s code unexpectedly, so they behave in overly safe manner (which overally is probably a good thing). In other words - they are afraid of possible side effects. In many cases they probably don’t even whitelist known globals as side effect-free.

What can even be tree-shaken? Mainly top level statements. Many statements consist of expressions though, which generally speaking are trickier to eliminate. Let’s consider simple:

export default class List extends React.Component {
	render() {
		return null
	}
}

What can be seen here:

  • single statement
  • no top level expressions

Result - tree-shakeable

Now consider the same but when classes are transpiled down to es5 (using loose mode for better readability):

var List = function (_React$Component) {
	babelHelpers._inherits(List, _React$Component);

	function List() {
		babelHelpers._classCallCheck(this, List);

		return babelHelpers._possibleConstructorReturn(this, _React$Component.apply(this, arguments));
	}

	List.prototype.render = function render() {
		return null;
	};

	return List;
}(React.Component);

export default List;

What can be seen here:

  • 2 statetements
  • first with a CallExpression

Result - not tree-shakeable

Maybe at this point we should go back a little and talk about what tree-shaking is doing in webpack. So as we know by default webpack wraps each module in a closure, it supplies module, __webpack_exports__, __webpack_require__ arguments so the module can register itself & require other modules from the global webpack’s module registry. When it detects that an export is a candidate for tree-shaking it simply removes the export statement and adds a comment like /* unused harmony default export */ (that is probably only for debugging etc), but it doesn’t actually remove the “tree-shaken” code. It shifts that responsibility on minifiers like UglifyJS which do dead code elimination later. In my opinion this is a huge mistake and part of the reason why rollup performs better when it comes to tree-shaking - it actually removes the “dead code”, or maybe it’s better said that it doesn’t include it, it only includes live code.

But going back to webpack - the export from second snippet gets marked as unused by webpack too, so why it doesn’t work? Because UglifyJS doesn’t know that it is safe to remove this call, it just has not sufficient information to determine statically that this IIFE does not produce any side-effects and that it is safe to remove it.

What can be done about it? Giving a small hint to UglifyJS about nature of this IIFE by adding #__PURE__ comment before the call. If we add it dead code elimination will be able to use it and remove it for us if the webpack has marked the export as unused before (thus it has skipped assigning it to __webpack_exports__).

🎉 babel@7-beta already adds those #__PURE__ annotations, although we might need to tweak it in babel, because not every class declaration is pure (computed properties etc can make them impure).

So the answer seems to be obvious - just add #__PURE__ and be done with it, right?

Unfortunately it is not that simple. Consider classes with static properties (and we need them for React components, no matter if we use a proposal class properties transform or not):

export default class List extends React.Component {
	static propTypes = {}

	render() {
		return null
	}
}

which transforms to:

var List = function (_React$Component) {
	babelHelpers._inherits(List, _React$Component);

	function List() {
		babelHelpers._classCallCheck(this, List);

		return babelHelpers._possibleConstructorReturn(this, _React$Component.apply(this, arguments));
	}

	List.prototype.render = function render() {
		return null;
	};

	return List;
}(React.Component);

List.propTypes = {};

export default List;

What can be seen here:

  • 3 statetements
  • first with a CallExpression again (could be marked as #__PURE__ now)
  • second is a property assignment

Result - not tree-shakeable even with #__PURE__

Why? Again - UglifyJS’ algorithms are not smart enough to know that a static assignment - List.propTypes = {}; - can be removed and while it is there whole List’ IIFE cannot be removed because List is used (by that static assignment).

Generally speaking it’s easier for minifiers to remove stuff (i.e. a class) if they are represented by a single statement, therefore this could be removed:

var List = /* #__PURE__ */ function (_React$Component) {
	babelHelpers._inherits(List, _React$Component);

	function List() {
		babelHelpers._classCallCheck(this, List);

		return babelHelpers._possibleConstructorReturn(this, _React$Component.apply(this, arguments));
	}

	List.prototype.render = function render() {
		return null;
	};

	List.propTypes = {};

	return List;
}(React.Component);

export default List;

🎉 but how to achieve such output? There are 3 options here:

  • wait for babel to do that - that is pulling statics into the closure (which is not trivial because class transform and class properties transform are independent) and marking classes as #__PURE__. This won’t happen for babel@6 though, but personally I’m already using babel@7-beta for number of projects and it works fine. Statics issue is not yet resolved, but I’m gonna fix this when I find time.
  • add this stuff manually in the source code - you’d have to wrap your classes in simple IIFEs so the outputted statics would land inside it. It would result in each (most) classes being wrapped by 2 IIFEs - 1 custom for statics, the class one that is already there. This doesn’t seem like a problem to me though.
  • implement custom transforms for both problems. For inserting #__PURE__ annotations there is already babel-plugin-annotate-pure-calls (disclaimer - I’ve written it). For dealing with static properties problem I’d argue that it is easier to write one which would do what got mentioned above - wrapping a class into second IIFE.

🎉 those things mentioned would help

…but unfortunately wouldn’t solve webpack’s problem entirely. As mentioned webpack is not even removing dead code and if you have a module like this:

/* 3 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";

/* harmony import */ var _Grid__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);

var List = /* #__PURE__ */ function (_React$Component) {
  babelHelpers._inherits(List, _React$Component);

  function List() {
    babelHelpers._classCallCheck(this, List);

    return babelHelpers._possibleConstructorReturn(this, _React$Component.apply(this, arguments));
  }

  List.prototype.render = function render() {
    return React.createElement(Object(_Grid__WEBPACK_IMPORTED_MODULE_0__["a" /* default */]), this.props);
  };

  List.propTypes = {};

  return List;
}(React.Component);

/* unused harmony default export */ var _unused_webpack_default_export = (List);
})

List will be removed correctly as unused, but List’s dependencies (Grid here) won’t. More can be read here (with standalone repo as example).

So what can be done about this?

  • advising people to use ModuleConcatenationPlugin, but it’s only available for webpack@3+ and people need to activate it
  • adding "sideEffects": false to your package.json, but it will only start to benefit people when webpack@4 gets out.
  • switch to flat bundles - that way UglifyJS will be able to remove dependencies of unused exports, because they will all live in a single scope and won’t be added to a scope with __webpack_require__ call (which is not tree-shakeable and even if it was it wouldn’t be able to remove the required module from the webpack’s registry)

NOTE There are also some other issues in react-virtualized which prevents proper tree-shaking, but those should be easier to fix.

  • requiring commonjs modules, such as classnames, babel-runtime/helpers
  • using a commonjs require (!) inserted by babel-plugin-flow-react-proptypes
  • prototype assignments such as this (again - things as single statement are easier to remove) and Object.defineProperty calls

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:10
  • Comments:19 (12 by maintainers)

github_iconTop GitHub Comments

2reactions
bvaughncommented, Jan 7, 2018

😂

1reaction
TrySoundcommented, Dec 28, 2018

It’s usually imported by user.

That’s my point. After this bug I realised how unsafe sideEffects is. And I don’t know the code of this project enough to add this field safely.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Reduce JavaScript payloads with tree shaking - web.dev
While improvements are continually being made to improve the efficiency of ... The term "tree shaking" comes from the mental model of your...
Read more >
Improving Site Performance With Webpack Tree Shaking
In this blog post, we'll discuss our approach to improving site performance with ES6 modules and tree shaking.
Read more >
Tree Shaking - SurviveJS
Starting from webpack 5, tree shaking has been improved and it works in cases where it didn't work before, including nesting and CommonJS....
Read more >
Possible improvements to tree shaking and bundle size #500
I have three observations about potential issues with Immer's bundle size and tree-shakeability: Immer went from 4.5K min+gz in 4.x, to around ...
Read more >
Tree Shaking - webpack
Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found