Webpack 的 tree-shaking 进阶之路

1,010 阅读3分钟

这是我参与更文挑战的第 2 天,活动详情查看: 更文挑战

Lynne,一个能哭爱笑永远少女心的前端开发工程师。身处互联网浪潮之中,热爱生活与技术。

前言

如果了解过我前面提到过 rollup 系列的这篇文章 - 无用代码去哪了?项目减重之 rollup 的 Tree-shaking,那你一定对 tree-shaking 不陌生了。

原本不支持tree-shaking 的 Webpack 在它的 2.x 也实现了 tree-shaking,好奇心又来了,Webpack 实现 tree-shaking 和 rollup 实现 tree-shaking 的原理是一样的吗?

因为这样的疑问,就有了眼前这篇文章。

Webpack 实现 tree-shaking

快速浏览完官方文档和众多文章后,发现 webpack 实现 tree-shaking 的方式还不止一种!但是,都与 rollup 不同。

rollup 是在编译打包过程中分析程序流,得益于于 ES6 静态模块(exports 和 imports 不能在运行时修改),我们在打包时就可以确定哪些代码时我们需要的。

webpack 本身在打包时只能标记未使用的代码而不移除,识别 代码未使用标记 并完成 tree-shaking 的 其实是 UglifyJS、babili、terser 这类压缩代码的工具。简单来说,就是压缩工具读取打包结果,在压缩之前移除未使用的代码。

那么 Webpack 实现 tree-shaking 的过程是怎样的呢?

2.1 初级 tree-shaking

关于最早版本的 Webpack 实现 tree-shaking 可以参考这篇文章 如何在 Webpack 2 中使用 tree-shaking,掘金也有翻译版,当然如果不愿意花时间考古,也可以看下面这一段总结:

  • UglifyJS 不支持 ES6 及以上,需要用 Babel 将代码编译为 ES5,然后再用 UglifyJS 来清除无用代码
  • 通过 Babel 将代码编译为 ES5,但又要让 ES6 模块不受 Babel 预设(preset)的影响:配置 Babel 预设不转换 module,对应地配置 Webpack 的 plugins 配置
  • 为避免副作用,将其标记为 pure(无副作用),以便 UglifyJS 能够处理
{
  "presets": [
    ["env", {
      "loose": true, // 宽松模式
      "modules": false // 不转换module,保持ES6语法
    }]
  ]
}
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
  })
]

2.2 tree-shaking 进阶 - Babili

Babili 是基于 Babel 的代码压缩工具,可以识别 ES6+ 的语法,所有 Babel 可以解析的语言特性它都支持。Babili 能将 ES6 代码编译为 ES5,移除未使用的类和函数,就像 UglifyJS 已经支持 ES6 一样。

2.3 tree-shsking 终极 - Terser

从 Webpack4.x 开始采用 terser 替代 UglifyJS 压缩代码,不仅解决了 UglifyJS 不支持 ES6 语法的问题,更是大大提高了速度。到目前最新的 Webpack5.x 版本,terser 已经内置于 Webpack 中,不再需要额外的配置。

webpack 4 没有分析模块的导出和引用之间的依赖关系。webpack 5 有一个新的选项 optimization.innerGraph,在生产模式下是默认启用的,它可以对模块中的标志进行分析,找出导出和引用之间的依赖关系。

同时,可以通过 package.json 有一个特殊的属性 sideEffects 告诉 Webpack 你的代码无副作用。

2.4 小结

但本质上 Webpack 的 tree-shaking 机制并不同于 Rollup。它会包含所有的代码,标记未使用的函数和函数,以便压缩工具能够移除。这就是对所有代码都进行 tree-shake 的困难之处。使用默认的压缩工具 UglifyJS / terser,它仅移除未使用的函数和变量;Babili 支持 ES6,可以用它来移除类,但必须特别注意第三方模块发布的方式是否支持 tree-shaking。

参考资料