Tree Shaking Examples

Katerina198b
5 min readOct 30, 2019

--

In 2016 Webpack v2 supported ES2015 module syntax, i.e. import and export syntax (or harmony modules) and announced the feature, called
Tree Shaking”.
The feature was created for DCE (dead code elimination), or if you put it another way, to help delete unused modules from the bundle.

1. Example

Let’s say we have files:

//abc.js
export function a() {..}
export function b() {..}
export function c() {..}
//index.js
import {a} from './abc.js'
a();

In this case, we don’t use functions b() and c(). So, it would be nice, if those functions weren’t exist in the final bundle.

2. Webpack v2 vs Webpack v4-v5

Webpack v2 and its earlier versions, all unused functions will be marked with \n/!* unused harmony export function_name *!/\n, but will not be deleted as dead code. This code can be removed with UglifyJS plugin. UglifyJS is connected separately with:

//webpack.config.jsplugins: [
new webpack.optimize.UglifyJsPlugin(),
]

Webpack v4 and Webpack v5 have the same behaviour in the development node with settings:

//webpack.config.json
optimization: {
usedExports: true,
}

In production mode, without any extra settings, dead code will be automatically deleted by Webpack v4 and v5:

//dist/bundle.js
(()=>{"use strict";console.log("A")})();

In production mode, dead code is deleted by terser-webpack plugin, which is turn on by default out of the Webpack v5 box.

3. How Webpack detects unused modules?

Let’s look at an example:

By specifying

//webpack.config.js
stats: 'verbose'

You can see in the console:

Webpack contains an own scope analyzer, which recursively extracts scopes from all figured modules in the project.

4. Side Effect vs Dead Code

Dead code is unreachable code.

A side effect is defined as a code that performs a special behaviour when imported, other than exposing one or more exports.

Let’s look at an example:

In this example, the final bundle must contain sideEffect.js module, because it set global variables, in other words, contains side effect.

In a 100% ESM module world, identifying side effects is straightforward. But now, as the documentation states, detect side effects in statements is a difficult task in JavaScript.

While exporting function works fine, React’s Higher-Order Components (HOC) are problematic in this regard.

In the example below, Webpack will include both functions into the final bundle, inpite of one of them is pure.

Terser actually tries to figure side effects out, but it doesn’t know for sure in many cases.

But we can help terser by using the /*#__PURE__*/ annotation. So a small change would make it possible to tree-shake the code:

export const toggle = /*#__PURE__*/ wrapperMutated('red')(el);dist/bundle.js
(()=>{"use strict";document.getElementById("toggle")})();

But there is another way to provide hints to webpack’s compiler on the “pureness” of your code by “sideEffects” option of package.json:

If there aren’t side effects, then:

//package.json
{
"sideEffects": false,
}

Or, if side effects are present in some file, for example ‘./src/side-effect-file.js’:

//package.json
{
"name": "your-project",
"sideEffects": ["./src/side-effect-file.js"]
}

Documentation about sideEffects: "If no direct export from a module flagged with no-sideEffects is used, the bundler can skip evaluating the module for side effects.".

So, when you are 100% sure that your file has not an important side effects you can to drop them with /*#__PURE__*/ or sideEffects: false options

Webpack v5 can also automatically flag modules as side-effect-free according to static analysis of the source code.

//sidEffectPure.js
export function logThree () {
console.log(3);
}
export function logFour() {
console.log(4);
}
/*#__PURE__*/
logFour();
//index.js
import {logThree} from './side-effect-module'
logThree();

In the example above, the final bundle will contain only logThree

But in some cases, for example in the code below, neither sideEffects: false nor /*#__PURE__*/ notation helps:

Lodash wasn’t removed from the bundle, becouse it use the CommonJS module system.

It’s sad, because lodash, and almost everything we export using npm are packages written with the Node runtime, and Commonjs is Node API for writing modules.

Webpack used to opt-out from used exports analysing for CommonJs exports and require() calls.
Webpack v5 adds support for some CommonJs constructs, allows to eliminate unused CommonJs exports and track referenced export names from require() calls.

I don’t want to get my hopes up but there could be a light at the end of the tunnel:

  1. Node has started supports ES6 modules in experimental mode. Try to run code with node — experimental-modules index.jsinstead of node index.js.
  2. 2. You can already find a replacement for some libraries with the one that uses ES6 syntax. For example, instead of “lodash” use “lodash-es

FAQ

I do the same experiments in my project, but nothing works! Why?

Possible, you use babel which replaces import on require (es6 → cjs). It makes no sense at all because obviously require or import doesn’t figure in the final bundle. Replacement of import to export was necessary for Webpack v1, which doesn't support es6 modules. You can turn it off with @babel/preset-env settings.

REPO
https://github.com/Katerina198b/tree-shaking

I hope you found this helpful.
If you’ve learned something new,
please clap 👏 button below so more people can see this

--

--