当前位置: 首页 > news >正文

体积减少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会寻找引入变量的引用,当发现没有对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中,有以下明确的定义:

// 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做到和其他静态语言一样好,靠这些工具是远远不够的,模块自身也必须配合做到符合规范。

文章出处:

  • github项目地址:github.com/vincentdcha…
  • 原文出处:vincentdchan.github.io/2018/05/bet…

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

  • 周刊文章集合: weekly
  • 团队开源项目: Feflow

相关文章:

  • CentOS7上Docker安装与卸载
  • webpack4.0各个击破(9)—— karma篇
  • day62:mysql主从配置
  • 网站服务器监控指标和日志收集
  • 从荣耀小米扎堆“滑盖全面屏”,看国产手机的“取巧”式创新
  • Dubbo配置方式详解
  • Python爬虫学习笔记(五)——XPath的使用
  • OSSEC安全监控环境搭建(docker+yum)安装
  • MySQL事务隔离级别、锁信息
  • 洛谷P2261 [CQOI2007]余数求和
  • Office 365发送超大附件
  • react-native 学习心得
  • 使用WPF技术模拟手机界面
  • 移动端常用的 UI 库
  • [转] 三种方法实现js跨域访问
  • 时间复杂度分析经典问题——最大子序列和
  • [原]深入对比数据科学工具箱:Python和R 非结构化数据的结构化
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • 07.Android之多媒体问题
  • Android系统模拟器绘制实现概述
  • Cumulo 的 ClojureScript 模块已经成型
  • ECMAScript6(0):ES6简明参考手册
  • Java新版本的开发已正式进入轨道,版本号18.3
  • k个最大的数及变种小结
  • Material Design
  • MyEclipse 8.0 GA 搭建 Struts2 + Spring2 + Hibernate3 (测试)
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • Octave 入门
  • Python进阶细节
  • ReactNativeweexDeviceOne对比
  • scrapy学习之路4(itemloder的使用)
  • SQLServer插入数据
  • 浮现式设计
  • 搞机器学习要哪些技能
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 每天一个设计模式之命令模式
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 容器服务kubernetes弹性伸缩高级用法
  • 入职第二天:使用koa搭建node server是种怎样的体验
  • 深度学习入门:10门免费线上课程推荐
  • 微信开源mars源码分析1—上层samples分析
  • 职业生涯 一个六年开发经验的女程序员的心声。
  • Android开发者必备:推荐一款助力开发的开源APP
  • ​queue --- 一个同步的队列类​
  • #宝哥教你#查看jquery绑定的事件函数
  • (2)nginx 安装、启停
  • (C语言)深入理解指针2之野指针与传值与传址与assert断言
  • (差分)胡桃爱原石
  • (第二周)效能测试
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (更新)A股上市公司华证ESG评级得分稳健性校验ESG得分年均值中位数(2009-2023年.12)
  • (全注解开发)学习Spring-MVC的第三天
  • (转)利用ant在Mac 下自动化打包签名Android程序
  • (转载)利用webkit抓取动态网页和链接