前端打包如何在减少请求数与利用并行下载之间找到最优解?

这题并不讨论是全部合并还是 Code Splitting 按需加载的问题…只考虑如何优化初始化所需要的 JS 文件: 现在很通用的做法是把所有依赖全部打包起来,只有一个 .js 文件,虽然 HTTP 请求数减少了,但是单个文件可能会非常大,虽然依赖清晰但是并不一定快 而如果拆分为比如 2~3 个 .js 文件,虽然请求数增多,但可以利用浏览器的并行下载特性来提升速度,只要控制一下执行顺序就好了(参考 LAB.js) 如何在这两者之间找到 sweet p…
关注者
641
被浏览
6,003
这可以是一个脑洞很大的问题,优化到极限是所有程序员的理想,但我觉得并不现实,也无必要。

我经历过的一些公司和方案

大众点评网:
那是2013年,点评网的前端技术还算是比较前沿的。我们有异步的模块加载器 kaelzhang/neuron · GitHub,有私有的包管理方案 Cortex · GitHub。我们几位技术的理想主义者,Kael +1 小马哥我们多次开会讨论前端模块化,前端加载器,前端性能优化的问题。比较理想的方案是:

  1. 代码全部 CommonJS 模块化;
  2. 采用 语义化版本 2.0.0 标准,
  3. 线上异步加载模块;
  4. 服务器根据各种页面对模块的需求情况通过算法合理的 combo 这些返回给模块加载器。

和题主的想法比较相近。虽然不太确定,但 1、2、3 我们是完成了,4 也许永远不会实现。我2013年已经离职,而 Kael、+1 不久前已经离职。翻翻点评页面上的代码(大众点评网_美食,生活,优惠券,团购 右键查看源码)还有当年理想的痕迹:
combo 的配置输出:
<script>
  var __loaderCombo = {
    '//http://www.dpfile.com/combos/~s~j~app~promo~placeholder.js,~s~j~app~main~placeholder.js,~s~j~app~main~mbox.js,~s~j~app~promo~mbox.js,~s~j~app~main~biz~mkt.js,~s~j~app~main~bulletin.js,~s~j~app~main~mkt.js,~s~j~app~main~tg-content.js,~lib~1.0~storage~local.js,~lib~1.0~storage~local-expire.js,~lib~1.0~mvp~tpl.js,~lib~1.0~dom~dimension.js,~lib~1.0~suggest.js,~lib~1.0~io~ajax.js,~lib~1.0~io~jsonp.js,~lib~1.0~util~cookie.js,~lib~1.0~util~queue.js,~lib~1.0~util~json.js,~lib~1.0~event~multi.js,~lib~1.0~event~live.js,~lib~1.0~switch~core.js,~lib~1.0~switch~conf.js,~lib~1.0~switch~tabswitch.js,~lib~1.0~switch~carousel.js,~lib~1.0~switch~autoplay.js,~lib~1.0~fx~tween.js,~lib~1.0~fx~easing.js,~lib~1.0~fx~css.js,~lib~1.0~fx~core.js/8b8f8f355aeac43833c8c3ce9c141175,8b8f8f355aeac43833c8c3ce9c141175,e57178e2684d3f7b36e0cc50abdeb01a,755028a19cabfa057e417a7718ededc2,61103b741ca56b4712da46f5556f3907,350a5fe49af6ab08f1307d8c26ff343d,334ea339327782c798b62b8a7916da33,fb0922bab163860af76cebf35fcf2ac6,8602861a2c191a9959f183138c097790,463c113fe9572f1ce5acbbff67710250,681c5b24a9a215968286adb35ea9a1b4,f12f839642deedcc2ef8e2235f146031,ea3b7ce0b29712205015c66468da7d85,85362489ccceac3fc3303ec569dd2b74,08440f9945a0f99cbbcadce7d5b140bf,afe6182c4f181e2d419ebec8c0026a69,f000da58a69731e4d966b79f319a973f,e54951fd409a1f2680a457e395b90dc1,7820a44330e04c9718005bfa97e80bc8,649a5074e678c2ca97609ade4c68ad5f,577271a07070095dc9c4398c1056b735,643e258aedf04b4bd5919ded8263191b,9aedd735203bac14d3da0420f278ee8b,4f1cd478d938e4ece4b5a6cb53b83b02,bb9c320f46054d5277a3aea90fd37747,97c9a39afa1a5d4bee3a0cfe8d5989f3,7e42281ab447ebdb115a133cc38cf03d,183b08c14447afc24ea8435a7500e020,d322a81f5d82047eb2b98912fe53c609.js': [
      '/s/j/app/promo/placeholder.js',
      '/s/j/app/main/placeholder.js',
      '/s/j/app/main/mbox.js',
      '/s/j/app/promo/mbox.js',
      '/s/j/app/main/biz/mkt.js',
      '/s/j/app/main/bulletin.js',
      '/s/j/app/main/mkt.js',
      '/s/j/app/main/tg-content.js',
      '/lib/1.0/storage/local.js',
      '/lib/1.0/storage/local-expire.js',
      '/lib/1.0/mvp/tpl.js',
      '/lib/1.0/dom/dimension.js',
      '/lib/1.0/suggest.js',
      '/lib/1.0/io/ajax.js',
      '/lib/1.0/io/jsonp.js',
      '/lib/1.0/util/cookie.js',
      '/lib/1.0/util/queue.js',
      '/lib/1.0/util/json.js',
      '/lib/1.0/event/multi.js',
      '/lib/1.0/event/live.js',
      '/lib/1.0/switch/core.js',
      '/lib/1.0/switch/conf.js',
      '/lib/1.0/switch/tabswitch.js',
      '/lib/1.0/switch/carousel.js',
      '/lib/1.0/switch/autoplay.js',
      '/lib/1.0/fx/tween.js',
      '/lib/1.0/fx/easing.js',
      '/lib/1.0/fx/css.js',
      '/lib/1.0/fx/core.js'
    ],
    '//http://www.dpfile.com/combos/~s~j~app~index~city.js,~s~j~app~main~datepicker~superdatepicker.js,~s~j~app~main~datepicker~supercalendar.js,~s~j~app~main~datepicker~calendarmodel.js/10e567965240627f31adeb03a0b5bb9d,d401bfe3cb080f56d3dd5477496085ce,d8f8da0738a40c79c6a6033059fc4f8a,f06dc4c4bf7930ab9e2d04b347c43684.js': [
      '/s/j/app/index/city.js',
      '/s/j/app/main/datepicker/superdatepicker.js',
      '/s/j/app/main/datepicker/supercalendar.js',
      '/s/j/app/main/datepicker/calendarmodel.js'
    ],
    '//http://www.dpfile.com/combos/~s~j~app~booking~common~datepicker~superdatepicker.js,~s~j~app~booking~common~datepicker~supercalendar.js,~s~j~app~booking~common~datepicker~calendarmodel.js,~s~j~app~booking~mainbookingplugin.js,~s~j~app~booking~reserveregion.js,~s~j~app~activity~vdperweekstarplugin.js,~s~j~app~hotel~index~hotel-shortcut.js,~s~j~app~main~app-2d.js/b3e3cb309221bc1b22a8840110270109,b5eee190bf95f12f9de6d7c22ac69122,f06dc4c4bf7930ab9e2d04b347c43684,1b1894cf4901ba0a3d9303a43b2977d4,9aa72d14ba3ca95811990f050e1bb11c,79cffb62b22c077b849431ba867e8b49,ab65fdf8fa47a417d9e87712210238df,eea8ab135c6a3be13b563abf9f3b706c.js': [
      '/s/j/app/booking/common/datepicker/superdatepicker.js',
      '/s/j/app/booking/common/datepicker/supercalendar.js',
      '/s/j/app/booking/common/datepicker/calendarmodel.js',
      '/s/j/app/booking/mainbookingplugin.js',
      '/s/j/app/booking/reserveregion.js',
      '/s/j/app/activity/vdperweekstarplugin.js',
      '/s/j/app/hotel/index/hotel-shortcut.js',
      '/s/j/app/main/app-2d.js'
    ]
  }
</script>
著名的 version.js : dpfile.com/x_x/version.,看了一眼,这个文件已经大到吓人的地步!

Teambition:我 2013年底加入,大型 SPA 应用。整个应用使用 RequireJS AMD 模块化。本地开发时异步加载,超过500个小的资源文件,页面刷新出来可能要10s 以上。所以调试一直是痛点。然,对于线上运行时优化特别少,三个阶段:

  1. 全部打包成一个 JS 文件;
  2. 分成两个 JS 文件,RequireJS 线上运行时 jrburke/almond · GitHub + 第三方依赖一个文件,业务代码一个文件;
  3. 三个文件,刚刚又看了一下代码:
    <script src="https://dn-st.teambition.net/libs/bundle/js/index.97e98c88.js"></script>
    <script src="https://dn-st.teambition.net/teambition/js/deps.7a49b762.js"></script>
    <script src="https://dn-st.teambition.net/teambition/js/app.f7e98219.js"></script>
    
    具体细节我已经不清楚了,但很可能 RequireJS 线上运行时 jrburke/almond · GitHub + 必须尽快执行的代码。

陆金所:首先,这是三家公司里面相对比较粗糙比较无脑的方案。
<script type="text/javascript" src="//static.lufaxcdn.com/lufax-public/jquery/jquery.7ebf933b.js"></script>

<script type="text/javascript" src="//static.lufaxcdn.com/lufax-components/lufax-components.726a6c1b.js"></script>

<script type="text/javascript" src="//static.lufaxcdn.com/lufax-public/lufax-lib/lufax-lib.1413b941.js"></script>
<script type="text/javascript" src="//static.lufaxcdn.com/lufax-public/lufax-public/lufax-public.972c53c4.js"></script>

<!-- <script type="text/javascript" src="http://hq.sinajs.cn/list=s_sh000001"></script> -->
<script type="text/javascript" src="https://static.lufaxcdn.com/home/index/8e0a02698e.index.min.js"></script>
<script type="text/javascript" src="//static.lufaxcdn.com/lufax-public/statistic/statistic.69b37206.js"></script>
一个页面中的 JavaScript 文件布局(CSS 也是类似,略去不表):

  1. jQuery,对,我们基本上都还是很基础的 jQuery 代码
  2. 公共组件,接下来的三个都是,只是更具不同的用途和级别做了划分
  3. 页面的业务 JavaScript:index.js
  4. 其他一些统计组件

当然这是表象,那我们代码的背后有什么模块化吗?

  1. 代码文件都是用立即执行的函数表达式(Immediately-invoked function expression) 包裹的吗?不是!
  2. SeaJS RequireJS AMD CMD?不是!
  3. ES6 Module?不是!
  4. 每个页面的业务代码基本上就是一个文件,采用全局命名空间实现组件化。

备注:有两三个项目采用 RequireJS,代码分模块开发的,但线上运行时并不是异步加载,而是按照依赖关系,每个页面合并成一个单独 js 文件(除去多个站点公用的部分);也就是说,在线上,两个页面间的 JS 文件里,有可能包含了很多相同的代码。

这些方案间的比较:

点评网
是三者中最牛逼,最理想的。但做起的复杂度超乎想象,这也可能是目前还没有完全实现的原因。可以看看 天猫tmall.com--上天猫,就够了 (kissy)、 支付宝 知托付!(sea.js) 好似都是这种风格的。

Teambition算是比较现实,SPA应用,比较与国际接轨的方案,毕竟是 SPA 应用。但可以看到,是这三个网站中打开速度最慢的。所有业务驱动的代码都在 JavaScript 中(HTML + 业务逻辑),有两种可选方案:

  1. 拆分为多个 SPA(推荐这个)
  2. 适当做一些异步加载

陆金所:开发(模块化啥的根本不需要知道,什么循环依赖根本不会出现)无脑,打包(grunt/gulp)无脑,访问网页看看,慢么,也不慢。

一些观点和结论:

模块化开发是趋势
:分而治之,是不变的道理。无论是传统网页(点评网、陆金所等)还是 SPA 应用,都需要借力模块化来保持代码的鲁棒性。解耦,独立,不会互相影响。
异步加载按需加载本身有点跑偏的:从 LAB.js 开始,各种各样的加载器都在追求加载性能,异步加载。希望可以加快页面的加载速度。分模块加载,异步加载的好处其实并没有那么明显,模块太多,或者异步加载,整体的加载实现反而延长。虽说 HTTP 2.0 能有效减少多个小文件加载消耗在网络上的时间,ES6 也原生提供 people.mozilla.org/~jor 的支持,但毕竟现在还没推广,效果也要实际使用才知道。
合理分组,同步加载,用好浏览器缓存和 CDN 应该可以解决大部分问题:区分开发运行时和线上运行时,开发时使用模块化,异步加载器大幅提升开发体验。线上按照代码更新频度和作用合理分组,合并压缩代码,同步加载三到五个文件。配置好静态服务器,使用 CDN,充分利用浏览器缓存和 CDN,静态资源就不会是性能的瓶颈了。

再说一句,
任何不以场景为前提的设计都是耍流氓,任何太不切实际的理想终将覆灭,任何想永存的技术都是临时方案(来自 @玉伯回复)。