Oleksandr Fediashov
Posted on May 14, 2020
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 }
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;
And then we just want to import buttonClassName
variable from it:
import { buttonClassname } from "./Button";
console.log(buttonClassname)
Let's compare the output produced with Webpack 4 with and without that line:
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;
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;
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 inprops
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?
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 👋
Posted on May 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.