Webpack 4 教程 - 7. 通过“tree shaking”减少打包的尺寸

在这一部分的webpack 4 教程中,我们继续进一步考虑优化。我们要学习“tree shaking”是啥以及如何使用。你会学到在webpack 4中使用“tree shaking”技术有什么要求,它能带来什么好处。开始吧!

Webpack 4 Tree Shaking

首先,我们回答“tree shaking”是啥技术以及它能带来什么好处。我们常常碰到这样的案例,需要从某文件中命名导出(某一个或几个变量、函数、对象等),然而这个文件还有许多其它(我们这次并不需要)的导出,webpack会不管三七二十一简单粗暴的将整个模块包含进来,使得我们最终打包的文件里有了许多不需要的垃圾。这就到了tree shaking出手的地方了,因为它能帮助我们干掉那些死代码,大大减少打包的尺寸。

如果你想对imports和exports了解更多,请查阅本教程1。

要想让tree shaking能“摇起来”,有几个要求,首先,必须使用ES6模块,不能使用其它类型的模块如CommonJS之流。如果使用Babel的话,这里有一个小问题,因为Babel的预案(preset)默认会将任何模块类型都转译成CommonJS类型。修正这个问题也很简单,不是在.babelrc文件中就是在webpack.config.js文件中设置modules: false就好了。

// .babelrc
{
  "presets": [
    ["env",
      {
        "modules": false
      }
    ]
  ]
}
// webpack.config.js

module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /(node_modules)/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['env', { modules: false }]
                }
            }
        }
    ]
},
如果你想阅读更多有关babel-loader或loaders的一般用法,请看本教程之2

第二个要求,需要使用UglifyJsPlugin插件。如果在mode:"production"模式,这个插件已经默认添加了,如果在其它模式下,可以手工添加它。

不熟悉UglifyJsPlugin插件的话,请参阅本教程5

另外要记住的是打开optimization.usedExports。在mode: "production"模式下,它也是默认打开了的。它告诉webpack每个模块明确使用exports。这样之后,webpack会在打包文件中添加诸如/* unused harmony export */这样的注释,其后UglifyJsPlugin插件会对这些注释作出理解。

Harmony 是 ES6 和 ES2015的代号。

我们来看看下面的情形:

// utilities.js

export function add(a, b) {
    return a + b;
}
export function subtract(a, b) {
    return a - b;
}
// index.js

import { add } from './utilities';

console.log(add(1,2));
console.log(add(3,4));

配置不恰当的运行就会有如下的输出:

/*(...)*/

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

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "substract", function() { return substract; });
function add(a, b) {
    return a + b;
}
function subtract(a, b) {
    return a - b;
}

/***/ })
/******/ ]);

正如你所见,webpack没有“摇”我们的包!这里既有add函数又有subtract函数。再使用如下的配置我们来体验一点点不同:

// webpack.config.js

const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const UglifyJS = require('uglify-es');

const DefaultUglifyJsOptions = UglifyJS.default_options();
const compress = DefaultUglifyJsOptions.compress;
for(let compressOption in compress) {
    compress[compressOption] = false;
}
compress.unused = true;

module.exports = {
    mode: 'none',
    optimization: {
        minimize: true,
        minimizer: [
            new UglifyJsPlugin({
                uglifyOptions: {
                    compress,
                    mangle: false,
                    output: {
                        beautify: true
                    }
                },
            })
        ],
    }
}

我关掉了UglifyJsPlugin插件的大部分选项配置以便更清楚的观察代码中发生了什么。运行之后有下面的输出:

/* (...) */

/* 0 */
/***/ function() {
    "use strict";
    // CONCATENATED MODULE: ./src/utilities.js
        function add(a, b) {
        return a + b;
    }
    // CONCATENATED MODULE: ./src/index.js
    console.log(add(1, 2));
    console.log(add(3, 4));
    /***/}
/******/ ]);

正因为optimization.usedExports和UglifyJsPlugin插件关掉的配置项,垃圾代码清除了。请注意,这是UglifyJsPlugin插件的默认行为,所以默认配置下使用UglifyJsPlugin也将清除死代码(除非运行了其它的压缩进程)。

库的Tree Shaking

如果你想要对库进行tree shake,首先要记住的注意点还是前面所说的:使用ES6模块。然而许多库并不一定使用ES6模块,典型的例子比如lodash就是这样,看它的源代码,很明显它没有使用ES6模块。
假设要使用lodash库中的debounce函数。

// index.js

import _ from 'lodash';

console.log(_.debounce);

现在你的输出里把整个lodash都打进来了。当使用import _ from 'lodash'时没有办法避免这一点。还好,有人创建了一个叫lodash-es的库,它改写lodash按ES6模块的形式导出。

import { debounce } from 'lodash';// (译注:原文如此,应该为'lodash-es')

console.log(debounce);

很不幸,webpack依旧没有对其tree shake。根据ECMAScript规范,所有的子模块都需要去评估,因为它们可能有副作用。我推荐读一下Sean Larking(他是webpack核心团队成员之一)在Stack Overflow上写的这篇很好的解释。库的作者可以在package.json文件里注明它的库没有副作用。打开lodash库的package.json文件,可以看到有"sideEffects": false的标注。那么这儿的问题是什么?

Webpack默认忽略了sideEffect标注,改变此行为需要设置optimization.sideEffects为true。你能手工设置它或通过设置mode:"production"模式也行。

// webpack.config.js

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'none',
    optimization: {
        minimize: true,
        minimizer: [
            new UglifyJsPlugin()
        ],
        usedExports: true,
        sideEffects: true
    },
    plugins: [
        new HtmlWebpackPlugin()
    ]
}

现在webpack终于对lodash库“摇”起来了。

小结

要让tree shaking好好工作,有一定条件。这么很有用的功能,当然值得学习。希望通过这篇文章,你会知道怎么去使用它,因为它能大大减少你打包的体积。记住需要使用ES6模块和UglifyJsPlugin插件,以及配置optimization选项,设置usedExports和sideEffects为true。

相关推荐