webpack 之 tree shaking

tree shaking 是由 rollup 作者提出并带火的,webpack 在版本2 的时候引入,tree shaking 属于性能优化的一种,国内通常翻译为摇树优化,tree shaking 本身属于 DCE 的一种(dead code elimination),其将未使用到的代码移除,从而使打包后的 js 文件体积变小。

tree shaking 之所以能够实现的原因是得益于 ES module 的提出,因为 ES 的模块规范是只允许 import 时的模块名是字符串常量,且模块的引用是一种强绑定,一种动态只读引用,也就是说 ES 的模块规范不依赖于运行时的状态,这使得静态分析能够是可靠的。

因此,如果你需要让你的 web 应用支持 tree shaking,那么你应该使用 es module 规范,而不是 commonjs 规范。

webpack 的 tree shaking 是依赖于 uglifyjs 实现的,因此应该将webpack 的 mode 设置为 production。

// webpack.config.js
module.exports = {
  mode: production
}

tree shaking 对于函数有效吗?

以一个简单的例子为例,主入口文件 index.js 依赖了 util.js 的一个方法,util.js 内定义了两个方法,如下:

// index.js
import { getName } from ./util


function main() {
  const name = getName()
  console.log(name)
}

main()
// util.js
function getName() {
  return huruji
}

function updateName() {
  return saber
}

export {
  getName,
  updateName
}

这个时候运行 webpack 进行打包,在输出文件中寻找 updateNamesaber 字符串,可以看到并没有找到,但是是能够找到 huruji 字符串的。说明 updateName 方法并没有被打包进去,说明对于函数是有效的

tree shaking 对于类方法有效吗

模块一般除了导出方法外,导出 class 也是非常常见的,如下改造一下 index.jsutil.js

// index.js
import { Name } from ./util


function main() {
  const name = new Name()
  console.log(name.getName())
}

main()
class Name {
  getName() {
    return huruji
  }

  updateName() {
    return saber
  }
}

export {
  Name
}

非常明显的是,这个时候并没有消除没有使用的方法。

这是因为没有标记为 sideEffectsfalse 的原因吗?原因当然不是的,真实的原因是大家根本没做这一块,具体的原因就是你根本无法确保这些方法就是只挂在了你定义的对象的 prototype 里面,还是影响了诸如 Array 之类的类里面(babel会将class转化为prototype),因此干脆不处理类方法,相关的讨论你可以在 rollup 的第 349 个issue 中看到,因此对于类的方法(包括静态方法)是不做 tree shaking 的。(你也可以在 Stack Overflow 的 are-static-typescript-class-methods-tree-shakeable-by-rollup 这个回答找到相应的说明)

tree shaking 能像传统的 DCE 一样清除不能达到的代码吗

将 index.js 改造为一下代码:

function saber() {
  console.log(saber)
}


function main() {
  if (false) {
    console.log(huruji)
  }
  return rin

  console.log(stay night)
}

main()

这里输出的 saber 方法没有被使用,hurujistay night 都是永远不会被输出的。运行 webpack ,编译打包之后可以发现没有找到相应的代码,因此,tree shaking 会把无法运行到的代码消除掉。

export default 对于 tree shaking 有影响吗

对于这个问题,先说结论,这完全取决于你的引入方式,import 进来的是整个对象,那么 webpack 就不会对你未使用到的方法做摇树优化,如下:

你可能经常会看到这样类似的写法:

/* eslint-disable  */
export function getName() {
  return huruji
}

export function updateName() {
  return saber
}

export default {
  updateName,
  getName
}

这样的好处在于你可以使用以下两种 import 方式使用其中的方法:

import util from ./util
util.getName()

// or

import { getName } from ./util
getName()

对于第一种写法本质上和 import * 没有什么区别。

如果你使用了第一种 import * 语法,那么无法去掉你未使用的方法,如果你使用了第二种引入方法,那么没有使用到的方法将会被优化掉,所以这完全取决于你。

如果你导出的方法中只是有一个方法被默认导出,如下:

export default function getName() {
  return huruji
}

export function updateName() {
  return saber
}

那么如果使用 import getName from ./util,因为引入只包含了 getName 方法,那么 updateName 这个方法就会被消除掉。

因此对于方法库,我不建议只默认导出一个对象,而是应该逐个方法导入,同时对于导入,应该尽可能只导入该模块使用到了的方法。

如果要规范所有同学都不应该在这种兼容写法上去导入这个默认的导出对象,那么可以开启 eslint 插件 eslint-plugin-importno-named-as-default-member 规则,这个规则会在你导入后使用的代码上做相应的提示。

import * as 对于 tree shaking 有影响吗?

结论是没影响,不同于导入默认模块的默认导出对象,这个对于 tree shaking 没有影响,因此这个使用是OK的,如下:

export function getName() {
  return huruji
}

export function updateName() {
  return saber
}

export default function deleteName() {
  return stay night
}
import * as util from ./util

function main() {
  const name = util.getName()
  console.log(name)
}

main()

如果导入的对象被赋值给了新的变量,会有影响吗?

答案是有的,因为这是一种 side effect,因为副作用的存在,所以 webpack 这时候并不会做摇树优化。

如下,即使你没有使用到 util2 这个变量,仍然无法消除掉未使用的方法

import * as util from ./util

const util2 = util

function main() {
  const name = util.getName()
  console.log(name)
}

main()

如何判定自己写的公共库是否有 side effects(副作用)

写公共库的时候,如果你能保证你的代码对于其他模块是没有影响的,如是否影响全局变量,是否影响原生对象,如果没有,那么就放心的在 package.json 中标记为 sideEffects: false

如何编写公共库来保证业务项目有更好的 tree shaking

遵循上面提到的几点,使用 es module 语法书写你的库,有多个导出方法或者对象时,不要只导出一个 default 对象,还需要在面向对象和函数单例中做好平衡,如果你的项目需要 UMD 规范导出,那么可以在 package.json 中通过 module 字段指定你的 ES module 规范的文件,webpack 会自动识别。

tree shaking 是否对于项目的优化有很大的帮助

对此,以我的经验来说,这个问题的答案是肯定的,一个项目中,尤其是大项目中,大量的公共utils方法存在,tree shaking 可以对此做大量的优化,笔者所在的项目组去年年底经过一次这样的优化,将大量的 commonjs 规范的模块和大量默认导出的模块进行了修改,效果显著。

同时,如果你的项目使用了 UI 库,那么 tree shaking 的效果会更加明显,像 Antd、elementui 都有诸如 babel-plugin-import 之类的插件优化,其实也可以看做是一种 摇树 优化,虽然其本质上是缩小组件的引用地址范围。

对于类方法的 tree shaking,是否能够做得更好

可以,可以尝试一下 google 开源的 closure-compiler,但这需要一些侵入式代码,并且由于这个工具是基于 java 的,可能会比较难和 node 生态融合,目前 google 也推出 node.js 版的,具体使用效果和上手难度可以期待我之后的使用体验。

© 版权声明
THE END
喜欢就支持一下吧
点赞66 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容