Webpack 的 tree-shaking 进阶之路(二)

这是我参与更文挑战的第 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

Uglifyjs.png

2号选手 BabelMinifyWebpackPlugin

BabelMinify.png

生产环境

1号选手 UglifyjsWebpackPlugin

生产U.png

2号选手 BabelMinifyWebpackPlugin

生产M.png

总结

今天主要讲的是 webpack 打包 + UglifyJS 压缩实现 tree-shaking,又从 babel 转译 + UglifyJS 压缩 优化到 直接使用 BabelMinify 实现。

剧透下明天要讲的内容:webpack 打包 + terser 压缩 实现 tree-shaking。

参考资料

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享