DEV Community

Oleksandr Fediashov
Oleksandr Fediashov

Posted on

How to kill tree shaking in Webpack with static properties?

Modern JavaScript bundlers like rollup.js and Webpack support the great feature that allows to decrease output bundle size and it's called tree shaking 🌲

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.

Usually it works like a charm, but sometimes something can go wrong... 🀭

Preface the problem πŸ“–

The common pattern for React components is to have some static properties like defaultProps, propTypes, etc.

function Button () {
  return <button />
}

Button.defaultProps = { primary: true }
Enter fullscreen mode Exit fullscreen mode

In our case (I am working on a UI library called Fluent UI React) we also have another static properties like className, create and few others. So what can go wrong? πŸ€”

Let's assume that we have such component:

function Button() {
  return null;
}

Button.defaultProps = {}; // This line breaks everything πŸ’£

export const buttonClassname = "ui-button";
export default Button;
Enter fullscreen mode Exit fullscreen mode

And then we just want to import buttonClassName variable from it:

import { buttonClassname } from "./Button";
console.log(buttonClassname)
Enter fullscreen mode Exit fullscreen mode

Let's compare the output produced with Webpack 4 with and without that line:

Comparison of Webpack bundles

Spoiler alert: Rollup handles this properly which can be checked in an interactive playground that I have created.

This issue is well described in webpack/webpack#8308 and short outcome is that:

Code like this g1.staticProperty2 = 'prop2'; can actually cause side-effects, if setters are registered.

Solution πŸ’‘

For classes it can be fixed by usage of babel-plugin-no-side-effect-class-properties which moves class properties definition to IIFE:

export default class Button {
  static className = 'ui-button';
}
// will be compiled to ➑️
var Button = /*#__PURE__*/function () {
  var Button = /*#__PURE__*/function Button() {
    _classCallCheck(this, Button);
  }
  Button.className = 'ui-button';
  return Button;
};

export default Button;
Enter fullscreen mode Exit fullscreen mode

In this case there will be no side effects as static properties now defined inside of IIFE.

What about functions? I haven't found any ready to be used solution yet. On our side we are still discussing the proper solution. To immediately fix this issue the following workaround can be applied:

const Button = (function () {
  const Button = function Button() {
    return null;
  };

  Button.defaultProps = {};
  return Button;
})();

export const buttonClassname = "ui-button";
export default Button;
Enter fullscreen mode Exit fullscreen mode

But it's obviously too hard to scale this approach. However, for React components it may be solved in a different way as there are two common static properties:

  • defaultProps can be fixed in two ways: for class components the Babel plugin can be used, for functional component I suggest to inline them in props destructuring as React team is going to deprecate them
  • propTypes can be removed from production bundles via babel-plugin-transform-react-remove-prop-types.

This post can be considered as a followup for library authors, for example Downshift.js met this issue previously.

Webpack 5?

Output by Webpack 5

I also tried the sample with webpack@5.0.0-beta.16 and it's the case there as well because Webpack relies on Terser for dead code minification.


As conclusion I would like to advice library authors rely on their tools as modern JavaScript toolkit is really powerful. But, at the same time keep your eye on produced bundle size πŸ¦… Bundlephobia and webpack-bundle-analyzer can help you there πŸ‘‹

Top comments (0)