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.
The webpack 2 release came with built-in support for ES2015 modules (alias harmony modules) as well as unused module export detection. The new webpack 4 release expands on this capability with a way to provide hints to the compiler via the "sideEffects"
package.json
property to denote which files in your project are "pure" and therefore safe to prune if unused.
Let's add a new utility file to our project, src/math.js
, that exports two functions:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
+ |- math.js
|- /node_modules
src/math.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
Set the mode
configuration option to development to make sure that the bundle is not minified:
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ mode: 'development',
+ optimization: {
+ usedExports: true,
+ },
};
With that in place, let's update our entry script to utilize one of these new methods and remove lodash
for simplicity:
src/index.js
- import _ from 'lodash';
+ import { cube } from './math.js';
function component() {
- const element = document.createElement('div');
+ const element = document.createElement('pre');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = [
+ 'Hello webpack!',
+ '5 cubed is equal to ' + cube(5)
+ ].join('\n\n');
return element;
}
document.body.appendChild(component());
Note that we did not import
the square
method from the src/math.js
module. That function is what's known as "dead code", meaning an unused export
that should be dropped. Now let's run our npm script, npm run build
, and inspect the output bundle:
dist/bundle.js (around lines 90 - 100)
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
Note the unused harmony export square
comment above. If you look at the code below it, you'll notice that square
is not being imported, however, it is still included in the bundle. We'll fix that in the next section.
In a 100% ESM module world, identifying side effects is straightforward. However, we aren't there quite yet, so in the mean time it's necessary to provide hints to webpack's compiler on the "pureness" of your code.
The way this is accomplished is the "sideEffects"
package.json property.
{
"name": "your-project",
"sideEffects": false
}
All the code noted above does not contain side effects, so we can mark the property as false
to inform webpack that it can safely prune unused exports.
If your code did have some side effects though, an array can be provided instead:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
The array accepts simple glob patterns to the relevant files. It uses glob-to-regexp under the hood (Supports: *
, **
, {a,b}
, [a-z]
). Patterns like *.css
, which do not include a /
, will be treated like **/*.css
.
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
Finally, "sideEffects"
can also be set from the module.rules
configuration option.
sideEffects
The sideEffects
and usedExports
(more known as tree shaking) optimizations 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. It is a difficult task in JavaScript and not as effective as straightforward sideEffects
flag. It also can't skip subtree/dependencies since the spec says that side effects need to be evaluated. While exporting function works fine, React's Higher Order Components (HOC) are problematic in this regard.
Let's make an example:
import { Button } from '@shopify/polaris';
The pre-bundled 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 "Does this code have any 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
or 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. This doesn't mean that terser is not doing its job well because it can't figure it out. It's too difficult to determine it reliably in a dynamic language like JavaScript.
But we can help terser by using the /*#__PURE__*/
annotation. It flags a statement as side effect free. So a small 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 are still questions with the imports which need to be included/evaluated because they could contain side effects.
To tackle this, we use the "sideEffects"
property in package.json
.
It's similar to /*#__PURE__*/
but on a module level instead of a statement level. It says ("sideEffects"
property): "If no direct export from a module flagged with no-sideEffects is used, the bundler can skip evaluating the module for side effects.".
In the Shopify's 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",
"./esnext/index.js",
"./esnext/configure.js"
],
// ...
For import { Button } from "@shopify/polaris";
this has the following implications:
Specifically per matching resource(s):
index.js
: No direct export is used, but flagged with sideEffects -> include itconfigure.js
: No export is used, but flagged with sideEffects -> include ittypes/index.js
: No export is used, not flagged with sideEffects -> exclude itcomponents/index.js
: No direct export is used, not flagged with sideEffects, but reexported exports are used -> skip overcomponents/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 itcomponents/Button.css
: No export is used, but flagged with sideEffects -> include itIn this case only 4 modules are included into the bundle:
index.js
: pretty much emptyconfigure.js
components/Button.js
components/Button.css
After this optimization, other optimizations can still apply. For example: buttonFrom
and buttonsFrom
exports from Button.js
are unused too. usedExports
optimization will pick it up and terser may be able to drop some statements from the module.
Module Concatenation also applies. So that these 4 modules plus the entry module (and probably more dependencies) can be concatenated. index.js
has no code generated in the end.
To better understand the impact of the sideEffects
flag, let's look at a complete example of an npm package with CSS assets and how they might be affected during tree shaking. We'll create a fictional UI component library called "awesome-ui".
Our example package looks like this:
awesome-ui/
├── package.json
├── dist/
│ ├── index.js
│ ├── components/
│ │ ├── index.js
│ │ ├── Button/
│ │ │ ├── index.js
│ │ │ └── Button.css
│ │ ├── Card/
│ │ │ ├── index.js
│ │ │ └── Card.css
│ │ └── Modal/
│ │ ├── index.js
│ │ └── Modal.css
│ └── theme/
│ ├── index.js
│ └── defaultTheme.css
package.json
{
"name": "awesome-ui",
"version": "1.0.0",
"main": "dist/index.js",
"sideEffects": false
}
dist/index.js
export * from './components';
export * from './theme';
dist/components/index.js
export { default as Button } from './Button';
export { default as Card } from './Card';
export { default as Modal } from './Modal';
dist/components/Button/index.js
import './Button.css'; // This has a side effect - it applies styles when imported!
export default function Button(props) {
// Button component implementation
return {
type: 'button',
...props,
};
}
dist/components/Button/Button.css
.awesome-ui-button {
background-color: #0078d7;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
dist/components/Card/index.js and dist/components/Modal/index.js would have similar structure.
dist/theme/index.js
import './defaultTheme.css'; // This has a side effect!
export const themeColors = {
primary: '#0078d7',
secondary: '#f3f2f1',
danger: '#d13438',
};
Now, imagine a consumer application that only wants to use the Button component:
import { Button } from 'awesome-ui';
// Use the Button component
sideEffects: false
in package.jsonWhen webpack processes this import with tree shaking enabled:
sideEffects: false
The result: The Button component will render, but without any styling since Button.css was eliminated during tree shaking.
To fix this, we need to update package.json to properly mark CSS files as having side effects:
{
"name": "awesome-ui",
"version": "1.0.0",
"main": "dist/index.js",
"sideEffects": ["**/*.css"]
}
With this configuration:
Here's how webpack evaluates modules during tree shaking:
Is the export from this module used directly or indirectly?
Is the module marked with side effects?
sideEffects
includes this file or is true
): Include the modulesideEffects
is false
or doesn't include this file): Exclude the module and its dependenciesFor our library's files with the proper sideEffects configuration:
dist/index.js
: No direct export used, no side effects -> Skip overdist/components/index.js
: No direct export used, no side effects -> Skip overdist/components/Button/index.js
: Direct export used -> Includedist/components/Button/Button.css
: No exports, has side effects -> Includedist/components/Card/*
: No exports used, no side effects -> Excludedist/components/Modal/*
: No exports used, no side effects -> Excludedist/theme/*
: No exports used, no side effects -> ExcludeThe impact of incorrect side effects configuration can be significant:
These issues can be particularly hard to debug because they often only appear in production builds when tree shaking is enabled.
A good way to test if your side effects configuration is correct:
It is possible to tell webpack that a function call is side-effect-free (pure) by using the /*#__PURE__*/
annotation. It can be put in front of function calls to mark them as side-effect-free. Arguments passed to the function are not being marked by the annotation and may need to be marked individually. When the initial value in a variable declaration of an unused variable is considered as side-effect-free (pure), it is getting marked as dead code, not executed and dropped by the minimizer.
This behavior is enabled when optimization.innerGraph
is set to true
.
file.js
/*#__PURE__*/ double(55);
So we've cued up our "dead code" to be dropped by using the import
and export
syntax, but we still need to drop it from the bundle. To do that, set the mode
configuration option to production
.
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- mode: 'development',
- optimization: {
- usedExports: true,
- }
+ mode: 'production',
};
With that squared away, we can run another npm run build
and see if anything has changed.
Notice anything different about dist/bundle.js
? The whole bundle is now minified and mangled, but, if you look carefully, you won't see the square
function included but will see a mangled version of the cube
function (function r(e){return e*e*e}n.a=r
). With minification and tree shaking, our bundle is now a few bytes smaller! While that may not seem like much in this contrived example, tree shaking can yield a significant decrease in bundle size when working on larger applications with complex dependency trees.
When working with tree shaking and the sideEffects
flag, there are several common pitfalls to avoid:
sideEffects: false
Setting sideEffects: false
in your package.json is tempting for optimal tree shaking, but this can cause problems if your code actually does have side effects. Examples of hidden side effects:
Consider this pattern:
// This file has side effects that might be skipped
import './polyfill';
// Re-export components
export * from './components';
If a consumer only imports specific components, the polyfill import might be skipped entirely if not properly marked with side effects.
Your package might correctly mark side effects, but if it depends on third-party packages that incorrectly mark their side effects, you might still encounter issues.
Tree shaking typically only fully activates in production mode. Testing only in development can hide tree shaking issues until deployment.
What we've learned is that in order to take advantage of tree shaking, you must...
import
and export
)."sideEffects"
property to your project's package.json
file.production
mode
configuration option to enable various optimizations including minification and tree shaking (side effects optimization is enabled in development mode using the flag value).devtool
as some of them can't be used in production
mode.You can imagine your application as a tree. The source code and libraries you actually use represent the green, living leaves of the tree. Dead code represents the brown, dead leaves of the tree that are consumed by autumn. In order to get rid of the dead leaves, you have to shake the tree, causing them to fall.
If you are interested in more ways to optimize your output, please jump to the next guide for details on building for production.