作者 | 左林编辑| 欧阳淑丽
前言
如果您读过 rollup 系列中的这篇文章,那么您一定熟悉 tree-shaking。 如果您对tree-shaking相关知识不熟悉,请点击上一篇文章,花5分钟了解:什么是tree-shaking。
众所周知,最初不支持tree-shaking的Webpack,在其2中也实现了tree-shaking,封装了减肥效果后,直到2.x才实现。 那么这两种实现Tree Shaking的原理是一样的吗?
带着这个疑问,写下了这篇文章。
Tree-shaking实现机制
快速浏览了官方文档和众多文章后,我发现在webpack中实现tree-shaking的方法不止一种! 而且,它们都与 rollup 不同。
早期webpack的配置和使用并不简单,因此一度被戏称为webpack配置工程师。 虽然现在webpack的配置已经大大简化,webpack4也号称0配置,但是如果涉及到复杂全面的打包功能,0配置是不够的。 实现了。 了解其功能原理和配置非常有用。 接下来我们来了解一下webpack实现tree-shaking的原理。
Tree-shaking--rollupVSWebpack
我们谈到了标记未使用的代码,也谈到了UglifyJS、babili、terser等压缩工具,那么webpack和压缩工具是如何实现tree-shaking的呢? 我们先来了解一下webpack中tree-shaking的前世今生吧!
Webpack 实现了 tree-shaking 的三个阶段。 第一阶段:UglifyJS
webpack标签代码+babel翻译ES5-->UglifyJS压缩并删除无用代码。 对于最早版本的Webpack实现tree-shaking,可以参考这篇文章如何在Webpack2中使用tree-shaking(参考文末链接地址)。 鹈鹕也有翻译版,其实如果你不愿意花时间去考古,你也可以看一下下面的总结:
// .babelrc
{
"presets": [
["env", {
"loose": true, // 宽松模式
"modules": false // 不转换 module,保持 ES6 语法
}]
]
}
// webpack.config.js
module: {
rules: [
{ test: /.js$/, loader: 'babel-loader' }
]
},
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: true
},
output: {
comments: false
},
sourceMap: false
})
]
第二阶段:BabelMinify
webpack标记代码-->Babili(即BabelMinify)压缩并删除无用代码。 Babili 后来更名为 BabelMinify。 它是一个基于Babel的代码压缩工具,Babel已经通过我们的解析器Babylon理解了新的句型,同时Babili中集成了UglifyJS的压缩功能,本质上实现了与UglifyJS相同的功能。 但使用babili插件时,就不需要翻译了。 而是直接进行压缩,使代码体积更小。
通常使用 Babili 来代替 uglify。 有两种形式:Babili插件和babel-loader预设。 官方文档最后指出,BabelMinify 最适合针对最新的浏览器(具有完整的 ES6+ 支持),也可以与通用 Babeles2015 预设一起使用,首先向上编译代码。
在 webpack 中使用 babel-loader,然后引入 minify 作为预设,会比直接使用 BabelMinifyWebpackPlugin 插件(接下来会提到)执行得更快。 因为babel-minify处理后的文件大小会更小。
第三阶段:Terser
Webpack 标签代码 --> Terser 压缩并删除无用代码(webpack5 已外部化) terser 是 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。 如果你读过这个issue(),你就会知道越来越多的人放弃uglify而转向terser,原因也很清楚:
在webpack 4中,需要自动配置压缩插件,但是最新的webpack 5已经对外实现了tree-shaking! 生产环境无需配置即可实现Tree-shaking!
Webpack的Tree-shaking过程
Webpack 标记代码
一般来说,webpack对代码进行标记,主要将import&export语句标记为三类:
首先我们要知道,为了正常运行业务项目,Webpack需要将开发人员编译的业务代码以及支持和部署此业务代码的运行时打包到产品(bundle)中。 在Webpack源码实现中,运行时生成逻辑可以定义为打包阶段的两个步骤:
事实上,代码的句子标记发生在依赖收集过程中。
标记运行时环境中的所有导入:
const exportsType = module.getExportsType(
chunkGraph.moduleGraph,
originModule.buildMeta.strictHarmonyModule
);
runtimeRequirements.add(RuntimeGlobals.require);
const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});n`;
// 动态导入语法分析
if (exportsType === "dynamic") {
runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport);
return [
importContent, // 标记/* harmony import */
`/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});n` // 通过 /*#__PURE__*/ 注释可以告诉 webpack 一个函数调用是无副作用的
]; // 返回 import 语句和 compat 语句
}
标记运行时环境中所有已使用和未使用的导出:
// 在运行时状态定义 property getters
generate() {
const { runtimeTemplate } = this.compilation;
const fn = RuntimeGlobals.definePropertyGetters;
return Template.asString([
"// define getter functions for harmony exports",
`${fn} = ${runtimeTemplate.basicFunction("exports, definition", [
`for(var key in definition) {`,
Template.indent([
`if(${RuntimeGlobals.hasOwnProperty}(definition, key) && !${RuntimeGlobals.hasOwnProperty}(exports, key)) {`,
Template.indent([
"Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });"
]),
"}"
]),
"}"
])};`
]);
}
// 输入为 generate 上下文
getContent({ runtimeTemplate, runtimeRequirements }) {
runtimeRequirements.add(RuntimeGlobals.exports);
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
const unusedPart =
this.unusedExports.size > 1
? `/* unused harmony exports ${joinIterableWithComma(
this.unusedExports
)} */n`
: this.unusedExports.size > 0
? `/* unused harmony export ${first(this.unusedExports)} */n`
: "";
const definitions = [];
for (const [key, value] of this.exportMap) {
definitions.push(
`n/* harmony export */ ${JSON.stringify(
key
)}: ${runtimeTemplate.returningFunction(value)}`
);
}
const definePart =
this.exportMap.size > 0
? `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
this.exportsArgument
}, {${definitions.join(",")}n/* harmony export */ });n`
: "";
return `${definePart}${unusedPart}`; // 作为初始化代码包含的源代码
}
}
压缩消除方法UglifyJS
以 UglifyJS 为例。 UglifyJS是一个js类库、最小化器、压缩器和美化器工具包(parser、minifier、compressororbeautifiertoolkit)。 详细介绍请参考UglifyJS英文指南。
如果你不想浏览这么大的短文档,你可以阅读干净直接的压缩配置参数摘要!
举个栗子:
plugins: [
new UglifyJSPlugin({
uglifyOptions: {
compress: {
// 这样该函数会被认为没有函数副作用,整个声明会被废弃。在目前的执行情况下,会增加开销(压缩会变慢)。
pure_funcs: ['Math.floor']
}
}
})
],
提示:如果名称在范围内重新定义,则不会重新测量。 比如varq=Math.floor(a/b),如果变量q没有被引用,UglifyJS就会杀死它,但是Math.floor(a/b)会被保留,没有人知道它做了什么。
事实上,在这么多的压缩配置中,不仅需要自动解决副作用,而且仅使用UglifyJS默认配置就可以消除无用的标记代码,从而实现tree-shaking。
特塞尔
以 terser 为例,terser 是 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。 详细内容请查看官方文档。 其实没有英文文档,一看就知道配置参数和UglifyJS没有太大区别。 事实上,还有更多的参数:
压缩性能PK
Webpack已经更新到版本5了,等等,我们的主题不是tree-shaking吗? 如何突然在压缩工具的道路上越走越远……
本质上来说,就是压缩工具实现了tree-shaking,所以我们看一下压缩工具的性能,看起来并没有什么问题!
TIP:压缩在生产环境生效,因此可以在生产环境进行tree-shaking。 以下 3 个可配置插件要求 webpack 版本至少为 V4+。
UglifyjsWebpack插件
基本用法变得更简单:
// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new UglifyJsPlugin()],
},
};
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
plugins: [
new UglifyJsPlugin()
]
}
BabelMinifyWebpack插件
通常使用babili来代替UglifyJS。 Babili 插件和 babel-loader 预设有两种形式。
巴比利插件
只需将uglify替换为Babili插件即可,此时就不需要babel-loader了:
// webpack.config.js
const MinifyPlugin = require("babel-minify-webpack-plugin");
module.exports = {
plugins: [
new MinifyPlugin(minifyOpts, pluginOpts)
]
}
babel-loader 默认值
官方文档最后指出,BabelMinify 最适合针对最新的浏览器(具有完整的 ES6+ 支持)webpack 显示时间,也可以与通用 Babeles2015 预设一起使用,首先向上编译代码。
在 webpack 中使用 babel-loader 然后导入 minify 作为预设会比直接使用 BabelMinifyWebpackPlugin 插件执行得更快。 babel-minify处理后的文件大小会更小。
即.babelrc中的配置如下:
{
"presets": ["es2015"],
"env": {
"production": {
"presets": ["minify"]
}
}
}
不过 BabelMinifyWebpackPlugin 插件的存在肯定会有其不可替代的作用:
使用第一种方法:
TerserWebpack插件
与 uglify 和 babelMinify 插件一样,terser 插件非常易于配置和使用。
webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
企业Momo截图_16247735356260.png
看来结果是符合预期的,而且由于我的文件代码本身体积就较小,所以在压缩包大小上的优势似乎并不显着,但是压缩时间还是比较显着的。
官方数据性能对比
来康康bableMinify文档中给出的对比:
包反应:
封装vue:
洛达什包:
包三.js:
概括
我们来看看问题区的网友是怎么说的:
总体思路是terser的压缩性能相比uglify提升了三倍! 好的!
总体思路是:鉴于 terser-webpack-plugin 得到维护,但有更多正确性修复webpack 显示时间,它绝对是首选 - 虽然没有性能提升(实际上是改进),但仍然值得切换。 最后一句话总结:webpack打包+terser压缩才是最终的选择! webpack5 外部简洁说明了一切!
处理副作用
“副作用”的定义是在导出时执行特殊行为的代码,而不仅仅是公开一个或多个导出。 比如polyfill,影响全局作用域,但一般不提供export。
副作用已经在 rollup 中引入了。 有些模块导出只要引入就会对应用产生重要影响。 例如,全局样式表或设置全局配置的 JavaScript 文件就是很好的反例。
Webpack 认为此类文件具有“副作用”,具有副作用的文件不应该进行 tree-shaking,因为这会破坏整个应用程序。 Webpack的tree-shaking在副作用处理上稍逊一筹。 它可以简单地判断一个变量是否被后续引用或修改,而无法判断一个变量的完整变化过程。 它不知道自己是否指向了外部变量,所以有很多可能性。 会产生副作用的代码只能保守地不删除。
幸运的是,我们可以配置项目来告诉 Webpack 哪些代码没有副作用并且可以进行 tree-shaking。
配置参数
在项目的 package.json 文件中,添加“sideEffects”属性。 package.json 有一个特殊的属性 sideEffects,它的存在是为了处理副作用 - 向 webpack 编译器提供关于哪些代码是“纯部分”的提示。 它有三个可能的值:
{
"name": "your-project",
"sideEffects": false
// "sideEffects": [ // 数组方式支持相关文件的相对路径、绝对路径和 glob 模式
// "./src/some-side-effectful-file.js",
// "*.css"
//]
}
每个项目都必须将 sideEffects 属性设置为 false 或文件路径字段。 如果你的代码确实有一些副作用,你可以提供一个字段来代替,并且 sideEffects 标签需要在工作中正确配置。
代码中的标记
你可以通过 /#PURE/ 注解告诉 webpack 函数调用没有副作用。 在调用函数之前将其标记为纯函数。 传递给函数的输入参数很难用刚才的注释来标记,需要将每个标记分开。 如果未使用的变量定义的初始值被认为是纯粹的,它将被标记为死代码,不会被执行并被压缩工具删除。 当 optimization.innerGraph 设置为 true 时启用此行为,并且 optimization.innerGraph 在 webpack5.x 中默认为 true。
句子使用水平
总结
参考
☞小米豪派“大红包”,人均 39 万元;约 200 家美国企业遭遇 REvil 勒索软件攻击;滴滴回应App被下架|极客头条