cycold / cycold.github.io

Please dot not star...
4 stars 1 forks source link

Conditional compilation, tree shaking and dead code elimination with webpack #156

Closed cycold closed 6 years ago

cycold commented 6 years ago

From https://www.thomann.io/blog/post/webpack_conditional_compilation_dead_code_elimination

Prerequisits For the purpose of a meaningful demonstration, I created a very simple demo project that can be accessed on the Thomann Github Account

First, we start with a "library" file called math.js that exports some functions to abstract the square and the cubic functions away:

// math.js

export function square(input) {
    return Math.pow(input, 2)
}

export function cubic(input) {
    return Math.pow(input, 3)
}

Then, the consumer of the function (index.js) imports the square method from the Math library and invokes it. In addition, a "debug message" is logged to the console:


// index.js

import {square} from './math'

if (process.env.NODE_ENV === 'dev') {
    console.log('calculating the squareroot of 10')
}

console.log(square(10))

Now we setup a development environment for webpack with this extremely simple configuration. Webpack takes the file src/index.js, bundles all its dependencies and outputs the result into build/indexDev.js:

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/build',
        filename: 'indexDev.js'
    }
}

When we run the webpack process, the result is an extremely cluttered and verbose Javascript file with almost 300 lines of code (I will not paste it in here...). For a development environment, this is fine because development is usually done on a local machine so we dont actually need to care about loading times. It is also desired that all the code we have written is retained (for example the debug message "calculating the squareroot of 10").

Enter webpack and its awesomeness:

Conditional Compilation

For the production build we setup a very similar webpack configuration with one little difference: We add the plugin field with the instantiation of the webpack EnvironmentPlugin (included in webpack).


const webpack = require('webpack')

process.env.NODE_ENV = 'prod'

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/build',
        filename: 'indexProd.js'
    },
    plugins: [new webpack.EnvironmentPlugin(['NODE_ENV'])]
}

This plugin has the task to look for variables in the ==process.env== object and evaluate if a condition appears to be currently met. In the configuration of the plugin's initiation we tell it look for the variable ==NODE_ENV== and we manually set the current environment of the webpack build process to 'prod'. Remember this passage from the index.js file?

if (process.env.NODE_ENV === 'dev') {...}

That's what the EnvironmentPlugin is looking for. It compares the current value of ==process.env.NODE_ENV== with the value that is used in the condition and replaces this condition if it does not match.

Let's see this in action! After running the prod build with the EnvironmentPlugin included, the output looks like this (this is only the relevant part of the build, the file is still kinda huge):

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

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__math__ = __webpack_require__(1);

if (false) {
    console.log('calculating the squareroot of 10')
}

console.log(__WEBPACK_IMPORTED_MODULE_0__math__["a" /* square */](10))

/***/ })

Notice the ==if (false) {...}== part? That means the EnvironmentPlugin has worked its magic.

Since the interpreter easily steps over the if (false) {...} part, it is not neccessary anymore to evaluate the condition at runtime in the production environment. Also, it has been defined at compile time that the code snippet inside the if condition is never to be shown in the production environment.

You might ask "why is the complete statement not removed altogether since it is never executed?" You are right, but keep reading until the =="Dead Code Elimination"== chapter!

Tree Shaking

Tree Shaking is a term that has been coined by the Rollup author Rich Harris. It means that the bundler is able to determine which part of an imported module is actually relevant and has to be kept in the resulting bundle. For this to be possible, the ES2015 module syntax is utilized. Because this feature has been introduced by rollup and many other bundlers have adopted it, in Webpack version 2 this functionality has been implemented as well.

Let's look at the result of the bundled math.js file, as output by the production build:

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

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = square;
/* unused harmony export cubic */
function square(input) {
    return Math.pow(input, 2)
}

function cubic(input) {
    return Math.pow(input, 3)
}

/***/ })

Though it looks a bit different than before, the code is easily discernible.

The whole module is wrapped into a function and therefore has its own scope where everything defined is only locally accessible.

The interesing part is this line of code

__webpack_exports__["a"] = square;

This is webpack's way of saying, "please export the following piece of code to the 'outside' world". This only happens for the square function (used by index.js), but not for the cubic function. The ES2015 module specifications makes it possible for Webpack, to understand that cubic is not relevant and therefore does not create a reference to it. It even leaves a comment (/ unused harmony export cubic /) which hints at the fact that the exported value is not used and will therefore not be exported.

这里的定义就是整个tree shaking的核心, webpack特别定义 __webpack_exports__["a"] = square; 也就是模块的定义的函数有被引用了, 而 声明cubic函数没有被引用, 所以就会成为 ==dead code==, 后面打包编译时就会被UglifyJsPlugin移除掉.

所以tree-shaking其实就是标明哪些函数有被引用.哪些函数变成了dead code 然后结合 代码压缩工具 将其移除掉.

所以 tree-shaking 是和 dead code 有关的, tree-shaking就是移除dead code.

In the next step, =="Dead Code Elimination"==, we will remove this piece of code completely.

Dead Code Elimination

For the last optimization step we add another plugin (also bundled with webpack) to our production configuration:

{
    ...
    plugins: [
        new webpack.EnvironmentPlugin(['NODE_ENV']),
        new webpack.optimize.UglifyJsPlugin()
    ]
    ...
}

UglifyJS is a tool that statically analyzes code, throws away unused code (hence the name "Dead Code Elimination") and minimizes its output. Webpack comes bundled with a plugin that utilizes UglifyJS.

After running the production build our output looks like this:

!function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(1);console.log(r.a(10))},function(e,t,n){"use strict";function r(e){return Math.pow(e,2)}t.a=r}]);

Okay, that is not helpful... The reformatted version looks like this:

!function (e) {
    function t(r) {
        if (n[r])return n[r].exports;
        var o = n[r] = {i: r, l: !1, exports: {}};
        return e[r].call(o.exports, o, o.exports, t), o.l = !0, o.exports
    }

    var n = {};
    t.m = e, t.c = n, t.d = function (e, n, r) {
        t.o(e, n) || Object.defineProperty(e, n, {
          configurable: !1,
          enumerable: !0,
          get: r
        })
    }, t.n = function (e) {
        var n = e && e.__esModule ? function () {
            return e.default
        } : function () {
            return e
        };
        return t.d(n, "a", n), n
    }, t.o = function (e, t) {
        return Object.prototype.hasOwnProperty.call(e, t)
    }, t.p = "", t(t.s = 0)
}([function (e, t, n) {
    "use strict";
    Object.defineProperty(t, "__esModule", {value: !0});
    var r = n(1);
    console.log(r.a(10))
}, function (e, t, n) {
    "use strict";
    function r(e) {
        return Math.pow(e, 2)
    }

    t.a = r
}]);

Much smaller than the output of the devlopment build, right?

Most of this is bootstrapping code from the webpack environment so let's look at the individual building blocks from the build of our original code.

This is all that's left of the index.js file. UglifyJS has realized that the line if (false) {...} is useless and had stripped it out completely:

function (e, t, n) {
    "use strict";
    Object.defineProperty(t, "__esModule", {value: !0});
    var r = n(1);
    console.log(r.a(10))
}

And this is what's left of our "library" math module.

function (e, t, n) {
    "use strict";
    function r(e) {
        return Math.pow(e, 2)
    }

    t.a = r
}

The square function is still there and is still exported (though now under the name r, the original name has been changed to use up less space). However, the cubic function has completely vanished. UglifyJS saw that there is no other code referencing the function and removed it since it will never be executed.

All the code above can be accessed on the Thomann Github Account so each step from this post can be traced.