Optimization stuff—webpack实践回顾

作者 Kylewh 日期 2017-04-25
Optimization stuff—webpack实践回顾

接触前端工程化以来,用的最多的就是webpack,起初对它的印象就是: 再也不用手动写一堆script标签了… 一个就好,看起来多酷!是不是有种封装的感觉! 接着便发现了一些问题, 比如: 打成一个包好大啊,要不要拆?怎么拆?到底拆成几个? error提示不再那么清晰! (想无耻的偷看别人的源码也变的困难,23333)随后就是对webpack有一个全面的了解,上述问题一个个都找到了解决方案。(光看文档真的会感觉像eatting shit啊…)

以下讲讲个人对webpack的一些理解,以及实践过的一些方案。(尽量不写代码,只谈思路和解决方案)

Webpack vs Gulp

一言以蔽之: Gulp是Task Runner, Webpack是Module Bundler

这句话就是区别所在,请好好体会语言的魅力。。。通过这个区别结合实践感受,可以得出的结论:

  • gulp的最小单位是task,自然控制粒度更小。
  • Webpack是一套依赖打包方案,无需code来指导操作,除了配置文件,实际上最原始的配置下,我们几乎只用指定一些目录。
  • 既然gulp的主战场在于task,那么对于task的内容,它能做的比webpack多,受益于海量插件,开发过程中的一些手动的繁琐操作都可以用gulp来编写来达到自动化流式处理。比如对于文件名的一些批量修改啊,上传,部署。(webpack也能做,但这不是它的主要功能,它只是顺手可以做,同样通过引入插件,但是自由度和定制度不如gulp灵活)
  • Webpack更擅长的是处理模块依赖关系,为何叫擅长? 你有见过拿gulp去分析commonChunk吗? Webpack可以对所有依赖进行分析并且提供可定制的处理方案,比如以何种规则进行commonChunk提取。 Code Split, tree Shaking, 都是基于webpack所擅长的领域而诞生的特性。
  • 二者是可以共存的,使用gulp对文件进行精细化操作,使用webpack对操作后的文件进行依赖打包。 可以发挥它们的最大威力。

Optimization

既然是工程化,讲求的是效率,性能。 效率上,自然是对于时间的缩减。 而性能,是一个综合的概念,平衡时间与空间。 一个个来说。

Size

更短的代码

对于文件大小,最直接的方案是对于最底层的优化: 压缩代码! 空格,换行,长变量名, 通通消灭。 编译更快了,load更快了,一举两得。 这是我们最先能想到的。 通常使用webpack自带的webpack.optimize.uglifyjsplugin

依赖重复

接着考虑依赖的优化,是否有重复? 首先要知道重复是如何诞生的。 A库的依赖树和B库的依赖树中都含有一个C module, 那么就有可能重复打包C。 于是webpack可以帮你找出这些重复的部分,丢掉!对应的工具是dedupe,现在它已经被移除,webpack2将默认进行这些优化。

emm…我们的size貌似变小了不少, 有没有可能更进一步?

冗余输出

从另外一个角度来思考,有没有可能存在一些冗余的代码? 它们的存在与否并不会影响应用的正常运行?典型:定义并输出,但未被引入。 这些无人问津的代码就像依赖树上的树叶,我们用力的抖一抖,由于它们不具有引用,就会掉落。 —–> 这就是tree shaking的形象比喻。 于是我们可以使用tree shaking进行冗余消除。

OK, 貌似我们做的不错, 文件是变小了很多, 但是我们可以做的更好。

Split

按需加载

我们并不需要一次性加载所有的代码,也许在首屏下我们只用到了相当少的模块,更多时候首页更像一个入口,真正的业务逻辑存在于分支里。 那么我需要考虑的是找到一个平衡: 如果我们的分支模块尺寸相对占比例较大,且各自独立,我们可以考虑按需加载。 这十分适用于路由系统。 OK,webpack里我们针对这种情况使用的方案是异步加载。 早期我们使用的是System.import(),现在已经原生支持import了,得益于babel的syntax-dynamic-import转义插件。 在react中,我们一般在指定route component的地方使用这种语法。查看输出我们会发现0.bundle.hashXXXX.js, 1.bundle.hashXXX.js等字样,在chrome network里也可以清晰的观察到不同路径下的请求发起。

重复依赖

Wait,既然是拆分,那么也可能产生重复问题吧? A -> C, B -> C, 按需加载A和B的时候,C都作为依赖被打包在各自的bundle里。 那么我们可以建立起公共引用,让AB公用一个C,而不是各自拥有一个。 这就涉及到CommonChunkPlugin的使用,可以看这个github上的例子,非常清楚。moving modules from child-chunks to the parent chunk

公共提取

还没完呢!考虑了在制作bundle过程中的优化,现在改换自己为一个用户的角色,当我使用网站访问APP的时候,我们下载了对应的打包文件,并进行缓存。 接着,作者提交了一个commit,进行了一些微弱的改动,可能只是一个不起眼的模块,然后打包,发布。 然后我们再次访问页面, 问题来了,由于文件更新了,我们不得不重新下载打包文件。

Boom! —-> 这就是关键所在, 我们浪费了大量时间在下载几乎同样的文件。

为了让我们可能会频繁改动的文件与那些我们几乎不会改动的文件分离开,我们需要进行抽取。 一般那些不会改动文件,往往是你node_modules下的依赖。 我们同样使用CommonChunkPlugin来进行提取, 有两种方式: 你指定vendor list。(比如react, react-router, react-dom…) 缺陷: 随着引入的外部依赖越来越多,你不得不频繁的取更新vendor list array. 所以还有第二种方式: 动态判定, 我们向插件传入一个函数,对外部依赖使用我们定制的规则进行分析,打包。 这个关键的rule,就是minChunk, 它的表义可以理解为: 最小引用次数。 当我们给其设置为一个数字的时候,代表所有引用次数大于等于这个数字的依赖都会被抽取打包成公共依赖。 当我们传入一个函数的时候,可以进行一些精细的抽取,具体见文档 当所有公共部分被单独打包的时候,我们的改动将与它们不再产生关联,自然,在你重新发布版本后,用户只需要去更新那些更新过的部分,而非整体reload。

更细致的提取

还有一些小问题:我们会发现,当我们改动codebase,打包,还是会生成不一样的vendor file啊!这是为什么? —> 我们忘记了webpack的操作, 每次运行时webpack会做一些事情来帮助完成任务,这些runtime code是会产生差异的。 所以,我们还需要再提取一个文件,通常我们叫它manifest,具体使用见文档,还有其他的解决方案DLLReferencePlugin,道理类似,这里有篇文章做了很好的示范: OPTIMIZING WEBPACK FOR FASTER REACT BUILDS

解放双手

Wait! 我还有问题! 每次打包,都会在输出目录里产生打包文件,我需要手动去删除,这很麻烦不是吗? 而且还有那些动态生成的文件名,我难道每次都要去html里copy&past吗!?好吧,鉴于频繁的打包更新,我们需要定制一个流程: 每次build前,删除输出目录,然后生成新的打包文件,并且自动在html文件里引入它们。 这里我们需要用到的工具是 rimraf,还有HtmlWebpackPlugin。 后者我们放入webpack.config.js里的plugins下, 前者我们则在npm scripts里写下这样的代码: build: rimraf --'你的输出目录' && webpack -p, 大功告成!

依赖本身

对于公共依赖,比如react,lodash,我们是否可以对其本身做一些优化?

按需依赖

我们只需要一个库中的很小一部分,这部分是独立的,那我们无需依赖整个库,而是这个特定的模块。比如import _ from 'lodash' 改为 import debounce from 'lodash/debounce'

替代者

得益于强大的社区生态,一些黑科技层出不穷,比如针对react,有人开发出了preact和inferno这样的替代品,它们号称比react更快,且尺寸更小。 我们可以直接修改依赖,而不需要对代码做相应修改,nice!性能&size的又一次双赢。

Speed

我们做了上述一系列优化,却发现我们需要等待很久,因为webpack要做的事更多了。 通常在开发状态下,我们使用devServer来观察每一次rebuild后的成果。这个过程里,有相当大一部分的时间浪费了,参见前面用户访问reload的原理。 我们有很多时间是在对同样的代码做同样的事情。 为了省下时间,同理也是应用缓存机制,对于没有改动的部分进行缓存,只取rebuild那些改动了的部分。 针对这个思路,除了上面提到过的DLLReferencePlugin,还有一个黑科技值得一提:Happypack, 它可以建立线程,加速我们的rebuild,并很好的利用缓存。首次build时速度甚至会变慢,这是由于happypack在进行分析和配置,当进入开发阶段,改动代码后的rebuild会快到让你吓一跳。Happypack

That’s all,在具体的开发中,要记住”Everything is tradeoff “, 在优化这个范畴下,没有绝对的好坏,只有取舍和平衡。 每一个优化都有可能为你带来一些问题,那么解决这些问题的成本是否超越了优化所带来的收益本身呢? 明确重点,把握时间和精力成本,才是优化的核心。 毕竟,工具,只是意志的辅助。

虽然这么说,还是期待更多黑科技,毕竟,程序猿总是在自虐和反省中惶惶终日嘛,233333