体积减小80%!释放webpack tree-shaking的真正潜力

在上周末广州举办的feday中,webpack的核心开发者Sean在介绍webpack插件系统原理时,隆重介绍了一个中国学生于Google夏令营,在导师Tobias带领下写的一个webpack插件,webpack-deep-scope-analysis-plugin,这个插件可以大大提升webpack tree-shaking的效率。前端

tree-shaking目前的缺陷

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

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

若是是接下来这种引入方式呢,我写了一个demo以下github

这个例子很是简单,若是用图来表示是这样 web

在index.js中引入了func.js中的func2,并无引入func1,可是func1引入了lodash。webpack检查的时候发现func.js中的确用到了lodash,因此不会把lodash去掉。实际上,咱们根本没用到它。json

webpack-deep-scope-analysis-plugin就能够解决这种判断。babel

插件效果

引入前 app

引入后 ide

85.8kb -> 不到1kb函数

固然,我这里是标题党了,由于这里直接把一个lodash库给去掉了,因此变化才这么惊人。可是即便在实际项目中,咱们也能轻易用一个插件减小大量的没必要要的引入。

原理

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

webpack的原理,其实就是遍历全部的模块,把它们打包成一个文件,在这个过程当中,它就知道哪些export的模块有被使用到。那咱们一样也能够遍历全部的scope(做用域),简化没有用到的scope,最后只留下咱们须要的。

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

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

// module scope start

// Block

{ // <- scope start
} // <- scope end

// Class

class Foo { // <- scope start

} // <- scope end

// If else

if (true) { // <- scope start
 
} /* <- scope end */ else { // <- scope start
 
} // <- scope end

// For

for (;;) { // <- scope start
} // <- scope end

// Catch

try {

} catch (e) { // <- scope start

} // <- scope end

// Function

function() { // <- scope start
} // <- scope end

// Scope

switch() { // <- scope start
} // <- scope end

// module scope end
复制代码

在ES6中,module是一种根做用域,只有function和class才能做为子做用域被导出,因此咱们解析的时候,不会把全部的scope都做为节点算进去。

咱们提到的这个webpack插件,正是内置了这样一个scope分析器,它可以从入口文件中分析出scope的引用关系,最后排除掉全部没有用到的模块。

固然,这个插件也并非本身作了全部的事情,它也是依赖于了前人的工做。 escope 是一个分析ES中scope的工具,插件做者将它改为了ts版本集成到了插件中,而且利用了webpack暴露的接口,能够解析出来的模块的AST树,基于这个AST就能够交给escope分析出scope的引用关系。

一些边际用例

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

状况一:重复赋值变量

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

import { isNull } from 'lodash-es';

var fun = 1;

fun = function scope(...args) {
  return isNull(...args);
}

export { fun }
复制代码

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

状况二:纯函数

// copy from rambda/es/allPass.js
import _curry1 from './internal/_curry1';
import curryN from './curryN';
import max from './max';
import pluck from './pluck';

var allPass = /*#__PURE__*/_curry1(function allPass(preds) {
  return curryN(reduce(max, 0, pluck('length', preds)), function () {
    var idx = 0;
    var len = preds.length;
    while (idx < len) {
      if (!preds[idx].apply(this, arguments)) {
        return false;
      }
      idx += 1;
    }
    return true;
  });
});
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作到和其余静态语言同样好,靠这些工具是远远不够的,模块自身也必须配合作到符合规范。

文章出处:


《IVWEB 技术周刊》 震撼上线了,关注公众号:IVWEB社区,每周定时推送优质文章。

相关文章
相关标签/搜索