前端打包如何在減少請求數與利用並行下載之間找到最優解?

這題並不討論是全部合併還是 Code Splitting 按需載入的問題…只考慮如何優化初始化所需要的 JS 文件:

現在很通用的做法是把所有依賴全部打包起來,只有一個 .js 文件,雖然 HTTP 請求數減少了,但是單個文件可能會非常大,雖然依賴清晰但是並不一定快

而如果拆分為比如 2~3 個 .js 文件,雖然請求數增多,但可以利用瀏覽器的並行下載特性來提升速度,只要控制一下執行順序就好了(參考 LAB.js)

如何在這兩者之間找到 sweet point,性能的最優解??

這裡還涉及到瀏覽器之間的差異:比如支持的最大並發數?是否要求非同域才能並行下載?桌面端和移動端的差異?……期待有這方面的經驗的高人來談一下最佳實踐


這可以是一個腦洞很大的問題,優化到極限是所有程序員的理想,但我覺得並不現實,也無必要。

我經歷過的一些公司和方案

大眾點評網:
那是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 的配置輸出:

&
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"
]
}
&

著名的 version.js : http://www.dpfile.com/x_x/version.min.v1446634429846.js,看了一眼,這個文件已經大到嚇人的地步!

Teambition:我 2013年底加入,大型 SPA 應用。整個應用使用 RequireJS AMD 模塊化。本地開發時非同步載入,超過500個小的資源文件,頁面刷新出來可能要10s 以上。所以調試一直是痛點。然,對於線上運行時優化特別少,三個階段:

  1. 全部打包成一個 JS 文件;
  2. 分成兩個 JS 文件,RequireJS 線上運行時 jrburke/almond · GitHub + 第三方依賴一個文件,業務代碼一個文件;
  3. 三個文件,剛剛又看了一下代碼:

    &

    當然這是表象,那我們代碼的背後有什麼模塊化嗎?

    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 也原生提供 https://people.mozilla.org/~jorendorff/js-loaders/Loader.html 的支持,但畢竟現在還沒推廣,效果也要實際使用才知道。

    合理分組,同步載入,用好瀏覽器緩存和 CDN 應該可以解決大部分問題:區分開發運行時和線上運行時,開發時使用模塊化,非同步載入器大幅提升開發體驗。線上按照代碼更新頻度和作用合理分組,合併壓縮代碼,同步載入三到五個文件。配置好靜態伺服器,使用 CDN,充分利用瀏覽器緩存和 CDN,靜態資源就不會是性能的瓶頸了。

    再說一句,

    任何不以場景為前提的設計都是耍流氓,任何太不切實際的理想終將覆滅,任何想永存的技術都是臨時方案(來自 @玉伯 的回復)。


    這是個 NP 完全問題。因子太多,能在有限範圍里用類似貪心演算法找到最優解。

    前要條件:

    RTT(Round-Trip Time) 往返時延。在計算機網路中它是一個重要的性能指標,表示從發送端發送數據開始,到發送端收到來自接收端的確認(接收端收到數據後便立即發送確認),總共經歷的時延。

    • 獲益:對同時使用 A/B 頁面節省一個 RTT 時間
    • 損失:對只使用 A 浪費 B 的大小
      • 位元組轉換為時間 = 位元組大小/帶寬
    • 收益:獲益 - 損失

    FROM: Static resource management optimization David Wei @ FB


    前端很多library 都是耍流氓 主要是沒有linker (不是minifier) Google closure compiler 有點接近了 個人覺得用js 寫的那些工具過幾年都會淘汰的 Babel webpack 之類的...


    只說方法不討論業務場景的問題都是耍流氓啊。

    優化第一要素,找到一項穩定的性能均值,然後無論做什麼,都以數據下降為目標。

    不可能面面俱到,只能因需而做。

    所以…先布點吧,收集緩存利用率,性能指標,求中位數,做理論上的優化,觀察曲線變化,再找一些極端情況下的場景優化。考慮這些打包載入的成本是否是整個系統的瓶頸,也是個問題。

    一般如果收集的數據好,是可以通過一些訪問數據推算出打包規則的,這方面的嘗試百度有,去年性能大會也有老外討論…

    不過對小公司效果不明顯,大公司收益高些。

    可能文不對題了,摺疊吧…


    樓蓋這麼高,戳天呀~


    Baidu EFE 的 blog 上有一篇講 SPA 系統構建的文章,裡面有提到一些相關的內容:《PC端大型單頁式商業內容管理系統的JS模塊化構建探索》。我們要權衡的是在全不合併(全部按需載入,最節約流量,但交互過程中的載入可能體驗並不好)和全部合併(全提前載入,可能有流量浪費,首屏可能會慢一些,但交互過程最流暢)之間找一個平衡點。不同類型系統的這個點肯定是不一樣的,就像 @魯小夫 和 @小爝 回答中說的,可能要先確定好優化的目標才好有針對性地去解決。

    甚至開腦洞的話還包括根據用戶的訪問數據動態去調整這個打包策略以獲得比經驗方法更接近理想狀態的結果。不過據我所知還沒有這方面成熟的理論和方案。


    比如這個問題的頁面

    vendor就是比較明顯的外部庫什麼jq什麼的按照你說的打包成一個文件載入。

    和其他的js也並行一起載入。當然這個頁面的文件比較少。如果頁面js需要的再多一點話。還需要跨域。

    之所以要打包成一個文件時因為你可以看到TTFB及其之前的時間的佔比其實是很高的。如果打包在一起明顯能節省時間。假如你的並行的文件多了。你就需要跨域啦。如果你覺得夠用就和當前頁面一樣不跨域咯。按照需要來做就好了。

    sweetpoint哪裡有固定的。都是看各個情況來尋找的。而且哪裡有這麼極致的sweetpoint,夠用就好了。


    ... 煞有介事把問題複雜化的人,是喜歡問題呢還是喜歡解決問題?

    如果你要極致JS性能,而又不想在請求數或文件尺寸任何一方妥協(我認為這最終是最簡單的方案),目前最好的工具仍然是https://developers.google.com/closure/compiler/?hl=en,這是一個多數前端來不急深究的工具,用好的很少。做過一個有2M JS代碼的應用,優化好可以compile到500k(ungzipped)以下,benchmark的結果比YUI Compiler和ugilify好50%以上。

    通常的項目JS代碼量達不到你需要特別關注的程度(compile以後),所以通常build成一個文件並沒有什麼不好, browserify和webpack不就是想干這件事嗎?用好HTTP cache和auto revisioning就可以了。

    況且,一般項目ugilify足夠了,實在太大的庫不會考慮build,而是考慮不同域名下的CDN,就不會造成同域名瀏覽器並發限制的問題了。


    我也來入坑吧。我用的是requirejs,剛開始時因為我的UI庫用用的UMD,r.js打包起來無能為力,並且文件部署都是md5+CDN所以緩存還是能利用上的,所以就沒合併。但最近因為某個與我無關的問題被某些人為了轉移注意力坑了一把,以問題與請求數太多可能有關而被事兒逼總監逼我優化。

    好了進入正題。完全不合併雖然能按需載入,但當模塊越切越細,請求數到達一個量級時問題就會暴露。

    進而,將文件適當地合併,就成了當務之急,但靜態合併問題就是要分析文件之間的依賴關係,公用的不打包,初始化文件打包。這樣能將請求數降低約50%,但從頁面性能看確達不到50%,也就20%。

    前面也說了全部打包一個文件,但這樣會有多餘的文件。

    然後大招來了,動態combo。這個其實服務端合客戶端我都寫好了。但好死不死硬碟掛了沒了。而且這有幾個問題:

    1.requirejs本身代碼對combo修改起來很費勁。

    2.combo 時不是告訴伺服器我需要哪堆模塊,而是告訴伺服器我需要A模塊,但合併時把我已經載入了的模塊去掉。這導致的問題是當我載入的模塊越多,URL就越長。

    優化講究的是合理,我們總監就TM天天只會拿著個pagespeed跑分,WTF


    所有js 合併到一個大文件,這在通常情況下是不好的。

    事實上我認為最重要的指標不是請求數和文件大小之類,而是 ttfb、首屏打開、dom ready和window onload 等時間節點,制定好指標,然後讓 KPI 發揮它的鞭策作用。


    @寸志 說的大眾點評網的優化思路應該算是接近我自己的理想狀況的了。

    這可以是一個腦洞很大的問題,優化到極限是所有程序員的理想,但我覺得並不現實,也無必要。

    優化到極限,其樂在體檢了過程中的樂趣,而且向極限著前行的時候應該也是進步的。我倒是希望大家的熱血不要被沒有必要和不現實所磨滅了。~相信我,優秀的前端工程師還是很多的~

    寸志所說的不現實是無法實現嚒?

    @小爝 說的這點也很重要~

    優化第一要素,找到一項穩定的性能均值,然後無論做什麼,都以數據下降為目標。

    在一次UC的分享會上:前端圈走進名企~UC前端技術專場回顧 有幸聽到了 @張雲龍 所介紹的模塊化開發體系,在我看來,這已經是一套完整的、優化比較全面的「打包」方案了。

    個人認為,打包的核心應該就是combo url + 資源依賴表。說起來簡單,但這其實是一件較為複雜,涉及的層面也比較多的事情。

    UC的這個工具其實是有很大的借鑒價值的:

    基於fis開發和封裝的自動化工具: Scrat - webapp模塊化開發體系

    線上栗子:2014年巴西世界盃

    其主要的思想是這樣的:

    1. 有一套資源依賴表,這個表是每次構建的時候通過通過``scrat``生成的,這裡包含了點評網的combo 的配置輸出:的相關信息。

    2. 當我們有了這個依賴表的時候,就有兩種方案進行選擇了。一是通過瀏覽器端的載入器,在``require.async("module",function(mudule){})``時,對依賴表進行分析,生成該模塊與其所依賴模塊的combo url進行載入;二是把依賴表交給伺服器,讓它分析模塊的載入路徑,然後寫死在頁面上,或者請求的時候生成combo url。scrat選擇了前者。

    3. 有一個配套的載入器,``scrat.js``。

    優點:

    1. 動態分析了需要載入的js模塊,可以實現按需的合併請求載入。

    require.async("a",function(a){
    a.do()
    elm.onclick = function(e){
    require.async("b",function(b){
    b.do(e)
    })
    }
    })

    2. 單個頁面不會進行多餘的js載入。

    3. 如果 ``require.async`` 的模塊是固定的,就能夠合理利用緩存。

    缺點:

    1. 多個頁面的緩存命中較低,不過可以通過適當的規範提高緩存命中率。

    暫時想到的就這麼多。逃

    更多參考和靈感,可以 @張雲龍 的博客fouber/blog · GitHub瞧瞧。


    這麼說的時候有考慮過緩存嗎……

    很多時候可以拆分公共和具體頁面內容 把後者打包 也方便開發也方便部署

    公共文件標上版本號讓它緩存唄

    問題是能不能很好地合理拆分全局或子系統的公共模塊

    有時候會糾結這個問題呢


    通常最好不要考慮這兩種優化,在業務穩固前。不按照邏輯進行的架構不利於敏捷開發。而好的運營是不會允許業務穩固的。所以就看你想不想加班、每次都進行這種所謂的優化了。


    當初用著 sea.js 想著各種怎麼優化打包,還好出坑了


    如果是前端渲染首屏的話,

    必須不能用載入器完成首屏渲染,

    必須阻塞渲染,

    至於打成幾個文件,需要自己測


    參考下RAILS。

    所以文件直接打包成一個大JS文件。簡單、粗暴、有效。第一次載入慢點,然後就好了。


    根據業務流情況,公用部分核心載入,其他部分按需載入,結合訪問量和伺服器負載進行調整,得到你客戶端能接受的最快載入時間。還是得砍實際業務情況的,沒辦法給出一個普適的最優解


    推薦閱讀:

怎麼樣向不懂前端的人介紹前端?
前端開發中,對圖片的優化技巧有哪些?
Google 的 HTML 代碼看著很亂,為什麼要寫成這樣呢?
網頁 head 標籤中的 JS 和 CSS,哪種文件放在前面,哪种放在後面比較好?
2016年前端技術將會呈現怎樣的局勢?全棧工程師是不是前端的一個趨勢?

TAG:前端開發 | 前端工程師 | 前端性能優化 |