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 进行打包,在输出文件中寻找 updateName
的 saber
字符串,可以看到并没有找到,但是是能够找到 huruji
字符串的。说明 updateName
方法并没有被打包进去,说明对于函数是有效的
tree shaking 对于类方法有效吗
模块一般除了导出方法外,导出 class 也是非常常见的,如下改造一下 index.js
和 util.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 }
非常明显的是,这个时候并没有消除没有使用的方法。
这是因为没有标记为 sideEffects
为 false
的原因吗?原因当然不是的,真实的原因是大家根本没做这一块,具体的原因就是你根本无法确保这些方法就是只挂在了你定义的对象的 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
方法没有被使用,huruji
和 stay 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-import 的 no-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 版的,具体使用效果和上手难度可以期待我之后的使用体验。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: lerna 发布失败后的解决方案
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
<export default 对于 tree shaking 有影响吗> 在这一块中的第一个示例,
我测试的结果是两者方式都不会删除没有用到的 updateName 函数,我的 webpack 版本是 v4.35.0。我猜测第二种 import 的方式不能删除没用的 updateName 是因为它赋值给了updateName 属性( export default { updateName }),产生了副作用,所以没有被删除
又能删掉 updateName了
副作用是指是否影响其他模块,你所说的函数引用是指? @supperpandababy
是不是可以这么认为?
只要是有函数引用,不管是哪种导入/导出方式都会产生副作用,从而无法被Tree-Shaking优化掉?