webpack配置文件中的 optimization 属性,用于集中去配置webapck内部的一些优化功能。
字面意思就是伴随着摇树的动作,树上的枯树枝和树叶就会掉落下来。
web开发术语 Tree Shaking 也是相同的道理,它表示「摇掉」代码中未引用的部分(未引用代码dead-code)。
MDN:Tree shaking 通常用于描述移除Javascript上下文中的为引用代码(dead-code)的行为。
它依赖于ES6中的import和export语句,用来检测代码模块是否被导出、导入,且被Javascript文件使用。
在webpack中就是将多个JS文件打包为单个文件时,自动删除未引用的代码。以使最终文件具有简洁的结构和最小化大小。
webpack生产模式中,自动开启了Tree Shaking这个功能。
示例:
// /src/component.js
export const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
export const Link = () => {
return document.createElement('a')
}
export const Heading = level => {
return document.createElement('h' + level)
}
// /src/index.js
// 只导入一个成员
import { Button } from './component'
document.body.append(Button())
使用生产模式打包后,Tree Shaking的效果就是,只将Button
打包进输出文件,其他两个成员由于未使用,而没有打包到输出文件。
// /dist/main.js
!(function (e) {
/*...*/
})([
function (e, t, n) {
'use strict'
n.r(t)
// 只有Button
document.body.append(document.createElement('button'))
},
])
Tree Shaking并不是 webpack 的某个配置选项。
它是一组功能搭配使用后的优化效果。
这组功能会在生产模式production下自动使用。
webpack官方文档对 Tree Shaking 介绍有些混乱,这里学习如何手动开启它,学习它的工作过程和优化功能。
上例代码,使用none
模式打包,查看打包文件,components.js
模块依然保留了Link
和 Heading
。
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// 导出了3个成员
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Button", function() { return Button; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Link", function() { return Link; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Heading", function() { return Heading; });
// components.js的内容全部打包进来了
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
const Link = () => {
return document.createElement('a')
}
const Heading = level => {
return document.createElement('h' + level)
}
})
通过配置optimization中的usedExports和minimize优化功能实现Tree Shaking。
usedExports: true
表示在输出结果中模块只导出外部使用了的成员。
打包查看输出文件:
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 只导出了Button
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
// Link和Heading被标记未使用
/* unused harmony export Link */
/* unused harmony export Heading */
// components.js的内容全部打包进来了
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
/** 没有用到的代码 start **/
const Link = () => {
return document.createElement('a')
}
const Heading = level => {
return document.createElement('h' + level)
}
/** 没有用到的代码 end **/
})
此时就可以通过压缩优化,删除掉「没有用到的代码」。
minimize: true
开启代码压缩优化。
删除注释、删除没有用到的代码、删除空白、替换变量名为简短的名称等。
它使用的是TerserPlugin或optimization .minimizer中指定的插件。
再次打包:
function (e, t, n) {
'use strict'
/*
压缩前:
__webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
*/
n.d(t, 'a', function () { return r })
/*
压缩前:
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
*/
const r = () => document.createElement('button')
}
webpack打包后,将每个模块放到一个函数中,其中包含对成员的定义 和 对成员的导出。
usedExports 可以标记模块导出的成员是否被外部使用,从而在打包结果中,不导出未使用的成员。
标记打包后表现为:包裹模块的函数中保留定义这些成员的代码,但是移除导出它们的代码,并添加注释/* unused harmony export */
。
而函数中没有了导出它们的代码,也就表示这些成员未使用,那定义它们的代码也没有了意义,minimize就会将这些未使用的定义成员的垃圾代码一并删除。
普通的打包结果,是将每个模块单独放在一个函数中。
如果模块很多,打包结果中就会有很多这样存放模块的函数。
开启concatenateModules: true
打包后,打包后的文件中,就不是一个模块对应一个函数,而是将所有模块都放在一个函数中。
concatenateModules
的作用就是尽可能的将所有模块合并输出到一个函数中。
既提升了运行效率,又减少了代码的体积。
这个特性又被称为「Scope Hoisting」,也就是作用域提升。
它是webpack3增加的特性。
由于webpack早期发展非常快,变化比较多。
当我们找资料时,找到的结果,并不一定适用于当前所使用的版本。
比如 Tree Shaking,很多资料中都表示 如果 使用了 babel-loader,就会导致 Tree Shaking 失效。
在这里统一说明一下。
首先要了解,Tree Shaking 实现的前提,是基于 「必须用 ES Modules 组织代码」。
即交给webpack处理的代码,必须使用 ESM 方式实现模块化。
MDN:Tree shaking 依赖于ES6中的import和export语句,用来检测代码模块是否被导出、导入,且被Javascript文件使用。
webpack优化的过程是先将代码交给loader去处理,然后再将处理结果优化输出。
而为了转换代码中的 ECMAScript 新特性,一般会选择babel-loader去处理JS。
而在babel-loader处理代码时,就有可能将代码中的 ESM 转换为 CommonJS。
PS:实际上这取决于是否使用了转换ESM的babel插件,而常用的插件集合 @babel/preset-env 就包含转换ESM的插件。
所以当 @babel/preset-env 工作时,代码中的 ESM 就应该被转换为 CommonJS。
所以webpack在打包时,拿到的就是 CommonJS 组织的代码,从而 Tree Shaking 也就不能生效。
但是现在的效果并不是这样。
在项目中使用babel-loader,并仅开启usedExports,查看打包结果。
module.exports = {
mode: 'none',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
optimization: {
usedExports: true,
},
}
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
// usedExports 生效了,也就是Tree Shaking 生效了
/* unused harmony export Link */
/* unused harmony export Heading */
var Button = function Button() {
return document.createElement('button');
console.log('dead-code');
};
var Link = function Link() {
return document.createElement('a');
};
var Heading = function Heading(level) {
return document.createElement('h' + level);
};
})
发现 usedExports 生效了,也就表示 Tree Shaking 并没有失效。
这是因为在最新版本的 babel-loader 中自动关闭了 转换ESM 的插件。
可以查看 node_modules/babel-loader
模块的源代码(/lib/injectCaller.js
)。
其中已经通过supportsStaticESM
supportsDynamicImport
标识了支持 ESM 和 动态import 方法(Webpack >= 2 supports ESM and dynamic import.
)。
"use strict";
const babel = require("@babel/core");
module.exports = function injectCaller(opts, target) {
if (!supportsCallerOption()) return opts;
return Object.assign({}, opts, {
caller: Object.assign({
name: "babel-loader",
// Provide plugins with insight into webpack target.
// https://github.com/babel/babel-loader/issues/787
target,
// Webpack >= 2 supports ESM and dynamic import.
supportsStaticESM: true,
supportsDynamicImport: true,
// Webpack 5 supports TLA behind a flag. We enable it by default
// for Babel, and then webpack will throw an error if the experimental
// flag isn't enabled.
supportsTopLevelAwait: true
}, opts.caller)
});
};
// ...
再去查看插件集合源代码(node_module/@babel/preset-env/lib/index.js
)。
翻到下面的代码:
const modulesPluginNames = getModulesPluginNames({
modules,
transformations: _moduleTransformations.default,
shouldTransformESM: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsStaticESM)),
shouldTransformDynamicImport: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsDynamicImport)),
shouldParseTopLevelAwait: !api.caller || api.caller(supportsTopLevelAwait)
});
看到preset-env根据babel-loader的标识,自动禁用了 ESM 和 动态import 的转换。
所以webpack通过babel-loader转换后,打包时还是ESM组织的代码,Tree Shaking也就能正常工作。
可以通过修改插件集合的配置,开启 ESM 转换:
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
// 注意:仅使用时,是将插件集合名称放到一个数组中
// presets: ['@babel/preset-env'],
// 当对插件集合编写配置时,就需要再套一个数组
// 数组的第一个元素是插件集合的名称
// 第二个元素是它的配置对象
presets: [
[
'@babel/preset-env',
{
// modules默认是auto,即根据环境去判断是否开启转换ESM插件
// 这里设置为强制转换为commonjs
modules: 'commonjs',
},
],
],
},
},
},
],
},
再次打包查看:
(function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Heading = exports.Link = exports.Button = void 0;
var Button = function Button() {
return document.createElement('button');
console.log('dead-code');
};
exports.Button = Button;
var Link = function Link() {
return document.createElement('a');
};
exports.Link = Link;
var Heading = function Heading(level) {
return document.createElement('h' + level);
};
exports.Heading = Heading;
})
3个成员都被导出,从而压缩优化时,也不会将它们删除。
通过以上实验发现,最新版本的babel-loader,并不会导致Tree Shaking失效。
如果还不确定,也可以尝试将preset-env配置中的modules
,设置为false
。
这样就会确保,preset-env不会开启 转换ESM 的插件。
同时确保了Tree Shaking工作的前提。