释放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 tree-shaking潜力之webpack-deep-scope-analysis-plugin_第1张图片

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

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

释放webpack tree-shaking潜力之webpack-deep-scope-analysis-plugin_第2张图片

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

释放webpack tree-shaking潜力之webpack-deep-scope-analysis-plugin_第3张图片

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

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

插件效果

引入前

释放webpack tree-shaking潜力之webpack-deep-scope-analysis-plugin_第4张图片

引入后

释放webpack tree-shaking潜力之webpack-deep-scope-analysis-plugin_第5张图片

85.8kb -> 不到 1kb

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

原理

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

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

释放webpack tree-shaking潜力之webpack-deep-scope-analysis-plugin_第6张图片

上图中, 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

你可能感兴趣的:(前端性能/框架)