释放webpack tree-shaking潜力之webpack-deep-scope-analysis-plugin

在上周末广州举办的 feday 中, webpack 的核心开发者 Sean 在介绍 webpack 插件系统原理时, 隆重介绍了一个中国学生于 Google 夏令营, 在导师 Tobias 带领下写的一个 webpack 插件, https://github.com/vincentdchan/webpack-deep-scope-analysis-plugin , 这个插件能够大大提高 webpack tree-shaking 的效率.

tree-shaking 目前的缺陷

tree-shaking 作为 rollup 的一个杀手级特性, 能够利用 ES6 的静态引入规范, 减少包的体积, 避免不必要的代码引入, webpack2 也很快引入了这个特性, 但是目前, webpack 只能做比较简单的解决方案, 比如:

这个例子中, webpack 会寻找引入变量的引用, 当发现没有对 isNumber 的引用时, 就会去除 isNumber 的代码. 这其实不太实用, 毕竟在现在的 vscode 中, 没有引用的变量在 ide 中都会灰显提示, 一般不会犯这种 import 某个模块却不用的错误了.

如果是接下来这种引入方式呢, 我写了一个 demo 如下

这个例子非常简单, 如果用图来表示是这样

在 index.js 中引入了 func.js 中的 func2, 并没有引入 func1, 但是 func1 引入了 lodash.webpack 检查的时候发现 func.js 中的确用到了 lodash, 所以不会把 lodash 去掉. 实际上, 我们根本没用到它.

webpack-deep-scope-analysis-plugin 就可以解决这种判断.

插件效果

引入前

引入后

85.8kb -> 不到 1kb

当然, 我这里是标题党了, 因为这里直接把一个 lodash 库给去掉了, 所以变化才这么惊人. 但是即使在实际项目中, 我们也能轻易用一个插件减少大量的不必要的引入.

原理

那么这个插件是怎么去解决这个问题的呢? 这里根据原作者在 Medium 上写的文章, 简单介绍一下他的做法.

webpack 的原理, 其实就是遍历所有的模块, 把它们打包成一个文件, 在这个过程中, 它就知道哪些 export 的模块有被使用到. 那我们同样也可以遍历所有的 scope(作用域), 简化没有用到的 scope, 最后只留下我们需要的.

上图中, func5 层层引用 fun4 fun3 fun2 fun1, 最后解析出来其实只使用了 deepEqual 模块.

什么是 scope 呢, 其实 scope 在各个语言中都有存在, 在 Wikipedia 中是作为计算机术语, 有更详细的解释, 我觉得可以翻译为作用域或者上下文, 在 ECMAScript 中, 有以下明确的定义:

 
  1. // module scope start
  2. // Block
  3. { // <- scope start
  4. } // <- scope end
  5. // Class
  6. class Foo { // <- scope start
  7. } // <- scope end
  8. // If else
  9. if (true) { // <- scope start
  10. } /* <- scope end */ else { // <- scope start
  11. } // <- scope end
  12. // For
  13. for (;;) { // <- scope start
  14. } // <- scope end
  15. // Catch
  16. try {
  17. } catch (e) { // <- scope start
  18. } // <- scope end
  19. // Function
  20. function() { // <- scope start
  21. } // <- scope end
  22. // Scope
  23. switch() { // <- scope start
  24. } // <- scope end
  25. // module scope end

复制代码

在 ES6 中, module 是一种根作用域, 只有 function 和 class 才能作为子作用域被导出, 所以我们解析的时候, 不会把所有的 scope 都作为节点算进去.

我们提到的这个 webpack 插件, 正是内置了这样一个 scope 分析器, 它能够从入口文件中分析出 scope 的引用关系, 最后排除掉所有没有用到的模块.

当然, 这个插件也并不是自己做了所有的事情, 它也是依赖于了前人的工作. https://github.com/estools/escope 是一个分析 ES 中 scope 的工具, 插件作者将它改成了 ts 版本集成到了插件中, 并且利用了 webpack 暴露的接口, 可以解析出来的模块的 AST 树, 基于这个 AST 就可以交给 escope 分析出 scope 的引用关系.

一些边际用例

凡事不能完美, 这个插件也有一些情况会导致判断失误

情况一: 重复赋值变量

比较典型的是以下这个例子:

 
  1. import { isNull } from 'lodash-es';
  2. var fun = 1;
  3. fun = function scope(...args) {
  4. return isNull(...args);
  5. }
  6. export { fun }

复制代码

这个例子中 fun 变量一开始被赋值为数字, 然后被赋值成一个函数, 但是 scope 分析器会直接跳过这个变量, 不把它当作一个单独的 scope.

情况二: 纯函数

 
  1. // copy from rambda/es/allPass.js
  2. import _curry1 from './internal/_curry1';
  3. import curryN from './curryN';
  4. import max from './max';
  5. import pluck from './pluck';
  6. var allPass = /*#__PURE__*/_curry1(function allPass(preds) {
  7. return curryN(reduce(max, 0, pluck('length', preds)), function () {
  8. var idx = 0;
  9. var len = preds.length;
  10. while (idx < len) {
  11. if (!preds[idx].apply(this, arguments)) {
  12. return false;
  13. }
  14. idx += 1;
  15. }
  16. return true;
  17. });
  18. });
  19. export default allPass;

复制代码

在这个例子中, import allPass 会导致_curry1 的运行, 因此它不会被当作一个单独的 scope, 因为它可能会有一些 "副作用", 比如改变某个全部变量, 对全局造成影响. 所以作者给了个方案, 可以在这个函数前加 /*#__PURE__*/, 这样就会把这个函数视为无副作用的纯函数, 如果我们没有 import allPass, 它引用的其他模块都会被去除.

最佳实践

首先, 要用到 tree-shaking, 必然要保证引用的模块都是 ES6 规范的. 这也是为什么我在前面的 demo 中, 引入的是 lodash-es 而不是 lodash.

在项目中, 注意要把 babel 设置 module: false, 避免 babel 将模块转为 CommonJS 规范. 引入的模块包, 也必须是符合 ES6 规范, 并且在最新的 webpack 中加了一条限制, 即在 package.json 中定义 sideEffect: false, 这也是为了避免出现 import xxx 导致模块内部的一些函数执行后影响全局环境, 却被去除掉的情况.

未来

当时跟这位插件作者沟通, 他说将来有可能 Tobias 会把这个插件内置到 webpack 中, 这也是符合 webpack4 零配置的趋势. 但是我们也看得到, 要将前端工程的 dead code elimination 做到和其他静态语言一样好, 靠这些工具是远远不够的, 模块自身也必须配合做到符合规范.

参考链接:

github项目地址:

https://github.com/vincentdchan/webpack-deep-scope-analysis-plugin

Medium原文出处:

https://medium.com/webpack/better-tree-shaking-with-deep-scope-analysis-a0b788c0ce77

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值