这是我参与更文挑战的第 4 天,活动详情查看: 更文挑战
Lynne,一个能哭爱笑永远少女心的前端开发工程师。身处互联网浪潮之中,热爱生活与技术。
前言
webpack 打包 + uglify 实现 tree-shaking 的分析来了!我说过我不会鸽哈哈哈哈…
不会长篇大论讲很多细节源码,想让我 5 分钟讲明白源码那是不可能的,主要是讲实现原理,涉及具体配置项思考。我们要研究的 tree-shaking 其实也只是打包和压缩工具中的其中一部分固有功能,而强大的工具本身也要稍作了解。
配置使用
前面我们也提到,webpack 打包 仅对import 和 export 的语句做标记,uglify 在压缩时才会删除无效代码。
具体对webpack的插件配置上一篇我已经讲过。除此之外,由于源码必须遵循 ES6 的模块规范,需要修改配置文件,指定 babel 处理 js 文件时不要将 ES6 模块转成 CommonJS 模块,要对 .babelrc 设置 babel-preset-es2015 的 modules 为 fasle,表示不对 ES6 模块进行处理。
// .babelrc
{
"presets": [
[
"es2015",
{"modules": false}
]
]
}
复制代码
Tip: 在webpack 3 和 4 不增加这个 .babelrc 文件也可以正常 tree shaking。
那么 webpack 是如何标记代码,而 uglify 又是如何对 webpack 打包标记的代码进行识别并 tree-shaking 的呢?
tree-shaking 实现流程
webpack 标记代码
总的来说,webpack 对代码进行标记,主要是对 import & export 语句标记为 3 类:
- 所有 import 标记为
/* harmony import */
- 所有被使用过的 export 标记为
/* harmony export ([type]) */
,其中[type]
和 webpack 内部有关,可能是binding, immutable等等 - 没被使用过的 export 标记为
/* unused harmony export [FuncName] */
,其中[FuncName]
为export 的方法名称
UglifyJS 压缩清除大法
UglifyJS 是一个js 解释器、最小化器、压缩器、美化器工具集(parser, minifier, compressor or beautifier toolkit)。具体介绍可以查看下 UglifyJS 中文手册
如果不想浏览这么一大长篇文档,就来看我干净利落、直指tree-shaking的总结吧!
- dead_code — 移除没被引用的代码 // 是不是很眼熟!无用代码!
- drop_debugger — 移除 debugger
- unused — 干掉没有被引用的函数和变量。(除非设置”keep_assign”,否则变量的简单直接赋值也不算被引用。)
- toplevel — 干掉顶层作用域中没有被引用的函数 (“funcs”)和/或变量(“vars”) (默认是false , true 的话即函数变量都干掉)
- warnings — 当删除没有用处的代码时,显示警告 // 还挺贴心有么有~
- pure_getters — 默认是 false. 如果你传入true,UglifyJS会假设对象属性的引用(例如foo.bar 或 foo[“bar”])没有函数副作用。
- pure_funcs — 默认 null. 你可以传入一个名字的数组,UglifyJS会假设这些函数没有函数副作用。
plugins: [
new UglifyJSPlugin({
uglifyOptions: {
compress: {
// 这样该函数会被认为没有函数副作用,整个声明会被废弃。在目前的执行情况下,会增加开销(压缩会变慢)。
pure_funcs: ['Math.floor']
}
}
})
],
复制代码
Tip:假如名字在作用域中重新定义,不会再次检测。例如var q = Math.floor(a/b),假如变量q没有被引用,UglifyJS会干掉它,但 Math.floor(a/b)会被保留,没有人知道它是干嘛的。
- side_effects — 默认 true. 传false禁用丢弃纯函数。如果一个函数被调用前有一段/@PURE/ or /#PURE/ 注释,该函数会被标注为纯函数。例如 /@PURE/foo();
事实上,在这么多的压缩配置中,仅使用 UglifyJS 默认配置即可去除无用标记代码以实现 tree-shaking。
UglifyjsWebpackPlugin
最新的 webpack5 版本保留了内置的 Uglifyjs 插件,即 UglifyjsWebpackPlugin。
基本的使用方式也更加简单:
// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new UglifyJsPlugin()],
},
};
复制代码
babili – BabelMinify
babili 被重命名为 BabelMinify,是基于 Babel 的代码压缩工具,而 Babel 已经通过我们的解析器 Babylon 理解了新语法,同时又在 babili 中集成了 UglifyJS 的压缩功能,本质上实现了和 UglifyJS 一样的功能,但使用 babili 插件又不必再转译,而是直接压缩,使代码体积更小(参考结论)。
一般使用 babili 替代 uglify 有 Babili 插件式和 babel-loader 预设两种方式。
Babili 插件式
只要用 Babili 插件替代 uglify 即可,此时也不需要 babel-loader 了:
plugins: [
new BabiliPlugin()
]
复制代码
babel-loader 预设
在官方文档最后有说明,Babel Minify 最适合针对最新的浏览器(具有完整的 ES6+ 支持),也可以与通常的 Babel es2015 预设一起使用,以首先向下编译代码。
在 webpack 中使用 babel-loader,然后再引入 minify 作为一个 preset 会比直接使用 BabelMinifyWebpackPlugin 插件(下一个就讲到)执行得更快。因为 babel-minify 处理的文件体积会更小。
即在.babelrc 中配置如下:
{
"presets": ["es2015"],
"env": {
"production": {
"presets": ["minify"]
}
}
}
复制代码
BabelMinifyWebpackPlugin
webpack5 中内置的 BabelMinify 插件 – BabelMinifyWebpackPlugin,使用方式也很简单:
// webpack.config.js
const MinifyPlugin = require("babel-minify-webpack-plugin");
module.exports = {
entry: //...,
output: //...,
plugins: [
new MinifyPlugin(minifyOpts, pluginOpts)
]
}
复制代码
UglifyjsWebpackPlugin VS BabelMinifyWebpackPlugin
在生产环境下打包作比较发现 – 当前最新版本的 webpack 中, 从实验中的打包时间上看,UglifyjsWebpackPlugin 更有优势,但从打包体积来看,BabelMinifyWebpackPlugin 略胜一筹。
开发环境
1号选手 UglifyjsWebpackPlugin
2号选手 BabelMinifyWebpackPlugin
生产环境
1号选手 UglifyjsWebpackPlugin
2号选手 BabelMinifyWebpackPlugin
总结
今天主要讲的是 webpack 打包 + UglifyJS 压缩实现 tree-shaking,又从 babel 转译 + UglifyJS 压缩 优化到 直接使用 BabelMinify 实现。
剧透下明天要讲的内容:webpack 打包 + terser 压缩 实现 tree-shaking。
参考资料
- UglifyJS 中文手册