合併 HTTP 請求是否真的有意義?

大家都說合併http請求是前端優化的共識,我們來分析一下,假如我們的頁面有100條http請求,http請求大概經歷這樣的過程: DNS定址、3次握手建立TCP連接、發送http報文、伺服器解析http請求、響應內容。

對於http1.0,我們有100 條請求,就得有100次這樣的重複過程;但在http1.1,keep-alive是默認的,而且現代瀏覽器都有DNS緩存,那麼對於「100條請求」和「對100條請求合併為1條請求」這兩種方案來說:

* DNS定址由於有DNS緩存--無差別;

* 3次握手由於有keep-alive,一條和一百條都只需一次TCP握手--無差別;

* 發送報文--增多了99次的http請求頭;

* 伺服器解析--無差別;

* 響應內容 --增多了99次的http響應頭;

只是增多了http報文頭,在實際應用中,是否有大的性能差別?


前面幾位的回答都挺全面了,也來湊個熱鬧。

首先,想了解這類問題,我強烈推薦閱讀Google性能專家 Ilya Grigorik 的書《Web性能權威指南 》,雖然沒有這個問題的完整解答,但基本都是書中的內容,不愧是大神,寫的非常非常非常好!

類似的這些問題可能都要從TCP/IP協議層面去考慮對性能的影響,我畫了一個圖展示這個問題的關鍵:

這個圖展示的是TCP中數據報文的發送和接收時序情況,左邊是keep-alive時不合併請求的情況,右邊是合併請求的情況。(這裡網路傳輸只考慮了網路延遲,假設網路環境極好,不存在丟包情況,每次服務端發送的DATA經過網路延遲之後客戶端都能按序接受並立即響應ACK,這樣為了避免示意圖的錯亂我就不把DATA發送過程中瀏覽器應答的ACK畫在圖中了,只保留最後一次的ACK)。

很容易注意到左右的差別吧,因為在keep-alive狀態下,即便復用連接,瀏覽器在文件與文件之間仍然要停一下發送一次HTTP的請求頭(就是那些由瀏覽器發出到達伺服器的GET a.js,GET b.js和GET c.js的報文),而右側合併了請求之後瀏覽器在接收內容的過程中沒有插入上行的數據傳輸,由此可見,網路延遲其實是在有keep-alive情況下仍然需要請求合併的主要動力。

由此我們可以得出結論,合併請求比keep-alive下不合併請求理論上能節省大概 RTT * (N - 1) 的載入時間(RTT是網路延遲,N是被合併的文件數量),當然,在網路延遲低的時候或者N很小的時候,都可能導致這種問題並不明顯,所以糾結與否取決於你的用戶網路。

如果考慮丟包(比如移動網路),合併請求會更有優勢,因為TCP報文會亂序到達,合併後的文件可以允許隊首丟包之後在中間傳輸過程中補上來,而分開資源的時候,前一個資源未載入完成後面的資源內容是不能載入的,會有更嚴重的隊首阻塞問題,所以 丟包率能進一步影響keep-alive下多個小文件的傳輸

另外,據說keep-alive在經過代理或者防火牆的時候可能會被斷開,所以也不能完全相信keep-alive就那麼穩定。

這裡只考慮了單個連接,現代瀏覽器都支持並發請求,問題可能不太一樣,要考慮瀏覽器的帶寬,這個結論可能會比較複雜,估計需要線上驗證。

當然合併請求也有弱點,就是請求合併的文件越多,每次版本發布後合併文件的緩存失效率就越高,瀏覽器緩存利用率相對偏低。

總結一下keep-alive時不合併請求的劣勢:

  • 文件與文件之間有插入的上行請求,增加了N-1個網路延遲
  • 受丟包問題影響更嚴重
  • 經過代理伺服器時可能會被斷開

不同的網路環境、HTTP協議版本和瀏覽器會有不同的優化策略這個說來實在話長,還是看書吧。


非FE,略懂HTTP、SPDY、HTTP2、HTTPS、TCP、瀏覽器及伺服器技術。所以從瀏覽器(以chrome為例)、網路協議(如果沒有特別指出,專門針對HTTP1.1)和網站伺服器(用於反向代理伺服器的nginx)的實現角度回答下樓主,合併HTTP的請求非常有意義

先逐個解答下樓主的疑問:

DNS定址由於有DNS緩存--無差別;

這裡有兩點差別:

  1. 即使有DNS緩存,瀏覽器也需要查找緩存,多個請求就需要查找多次,而且緩存有可能被無故清空,這樣多個請求的DNS查詢有可能花費更多時間。

  2. 即使忽略DNS查詢時間的差別,也有可能需要消耗URL的安全檢查時間。chrome有一個safe browsing的機制,會檢查每個資源的URL安全性,如果URL比較奇怪(比如一串很長的無意義的字元),就很有可能被安全檢查,如果本地資料庫查詢不到,有可能上傳無端資料庫,甚至HANG住5秒鐘。筆者就曾經遇到過這樣的問題,最後通過和chrome官方團隊溝通解決了,幫助我的也正是《Web性能權威指南 》的作者,Ilya Grigorik。

當然上述兩點的影響相對很小,其實我也覺得勉強,但是確實遇到過了。如果用戶某條請求突然載入很慢,有可能是瀏覽器端出現了問題,chrome雖然性能不錯,但BUG也不少,請求數量大,觸發BUG的機會也多。比如我上文提到的BUG,如果不是瞎折騰多出了那麼多的URL也不會遇到。

3次握手由於有keep-alive,一條和一百條都只需一次TCP握手--無差別;

TCP握手時間確實沒差別,但時間性能上差別非常大。

  1. HTTP1.1協議規定請求只能串列發送,這也是HTTP性能最差和最讓人詬病的地方,也就是說一百個請求必須依次逐個發送。第80個請求必須依賴於第79個請求正常返回之後才能發送。這樣就平白無故多出了99個網路RTT。PC上的RTT大概是50ms, wifi 為100ms, 3G為200ms,2G為400ms。你說這樣能快嗎?
  2. HTTP1.1確實支持pipeling,但pipeling有兩個問題:
    1. 部分廠商和代理早期pipeling的實現有缺陷,導致pipeling的部署推廣緩慢,但現在這個問題應該得到了一定的改善。
    2. pipeling的原理是客戶端可以並行發送多個請求,但是伺服器的響應必須按次序返回。也就是說假設100個請求可以從瀏覽器同時發送,但是伺服器即使先收到了第100個請求,也必須在第99個請求響應完後才能發送,這中間任何請求處理出現問題都達不到並發的效果。所以在實際應用過程中,pipeling沒有什麼顯著收益。這也是為什麼直到現在很少有瀏覽器或者伺服器真正使用pipeline的原因。

* 發送報文--增多了99次的http請求頭;

樓主可以自己抓包看看幾個大網站比如tmall, baidu的請求頭部大小。我補充幾個數據。

  1. 請求的平均頭部大小是800個位元組左右,並且會隨著ua和cookie的不斷增加而增加。數據來源:http://dev.chromium.org/spdy/spdy-whitepaper
  2. 無線環境下頭部大小每減少100個位元組,速度能夠提升20~30ms。為什麼呢?

    1. 上下行帶寬嚴重不對稱,上行帶寬太小。在部分運營商或者網路環境下,上行帶寬可能只有幾十個位元組。假設一個請求頭部是800個位元組,如果上行帶寬是100個位元組,那至少得傳8次才能將一個請求傳完。補充一下,上下行帶寬不對稱主要是技術和市場原因決定的,倒不是運營商太奸詐。
    2. 上述數據實際測試得到,不要問我為什麼。

* 伺服器解析--無差別;

大型網站的伺服器解析主要有如下地方耗時:

  1. HTTP協議解析。如果每次請求都是一個完整的HTTP請求的話,伺服器需要解析出完整的頭部、請求體。對於nginx這樣的高性能伺服器來說,這塊最多也就消耗幾毫秒。
  2. 域名查找和分流。也就是nginx需要把對應url的請求分配到對應的upstream上(主要針對反向代理伺服器)。如果請求數多,浪費在分流上的時間也不少。
  3. 負載均衡。通過輪詢或者哈希發送給不同的後端。
  4. WAF,應用層安全檢查,需要對不同的URL,cookie進行過濾和篩選。遇到複雜的規則,非常消耗時間。

顯然,如果只是一條請求,只有一次耗時。但如果是多條請求,則會有多次解析、分流、負載均衡、安全檢查等過程。很多小網站沒有負載均衡和WAF檢查,但至少有協議解析和分流的耗時。

上述還只是考慮了NGINX,殊不知還有很多低性能的伺服器實現,包括各種JAVA,甚至PYTHON,RUBY的伺服器。浪費在伺服器處理上的時間不容小視,幾十甚至幾百毫秒很正常。

* 響應內容 --增多了99次的http響應頭;

響應內容增加的響應頭相比請求頭部的影響要小一些,因為下行帶寬一般都是上行帶寬的數倍。相比請求頭部增加的耗時,響應頭部增加的時間可以忽略。

好,說了這麼多,其實樓主問到的瀏覽器通過一個TCP連接發送100個請求的事情根本就不可能發生。原因是:

1,儘管HTTP1.1協議建議每個域名建立2個連接(rfc2616),但是瀏覽器為了彌補HTTP串列發送的劣勢,完全沒有理會這個RFC。下面是常用瀏覽器針對每個域名並發建立的最大連接數:

也就是說現在基本都是6個連接。假設一個頁面只有一個域名,包含100個資源,瀏覽器也會默認建立6個TCP連接,然後每個連接上串列發送若干個請求。其實瀏覽器的實現也從側面證明了,並發才是王道。

那問題來了,既然100個請求,那為什麼不建立100個TCP連接一起發送出去?

  1. 會顯著增加瀏覽器和伺服器的網路資源消耗。特別是伺服器需要和N個客戶建立N*100個連接,壓力會增加非常多。

  2. 由於TCP的慢啟動及擁塞控制窗口等的限制,多個TCP連接的性能並不一定就能帶來WEB網路傳輸上的優勢。綜合來講,推薦6個。

HTTP Archive 的數據可以看出,平均一個域名有18個TCP連接。所以為了突破瀏覽器的限制,大部分網站使用了域名分片,domain sharding。即建立多個域名,這樣每個域名都能建立6個連接,就能提升並發連接數了。但是這個有如下缺點:

  1. 增加域名管理、IDC容災和運維的難度。這個是大部分FE考慮不到的,但很重要。
  2. domain sharding推薦的數量是2個,可以參考這個數據:http://www.mobify.com/blog/domain-sharding-bad-news-mobile-performance/ 和這篇文章:Domain Sharding revisited
  3. 盲目增加TCP連接數會極大破壞TCP擁塞機制。因為TCP擁塞和流控的前提假設是建立在一個TCP連接上的,如果同時建立多個TCP連接,對於多個連接並發流量的突增和暴漲,TCP無法有效控制。

此外,如果考慮到HTTPS協議,建立多個TCP和SSL連接需要消耗非常多的計算資源,對用戶速度 影響非常大。可以參考之前的一篇文章:大型網站的 HTTPS 實踐(二)——HTTPS 對性能的影響

既然有人提到了,就再說下SPDY和HTTP2,核心優勢就是多路復用。簡單說來就是將多個請求通過一個TCP連接發送,其實就是樓主迷惑的答案。

就算能多路復用,瀏覽器能不能將100個請求通過一個TCP連接發送?會出現什麼問題?其實有一個比較麻煩的問題,這個問題在SPDY、HTTP2協議下會更加嚴重。那就是TCP協議的head of line blocking,隊頭阻塞。

設想這樣一個場景,一個頁面有100個請求,第99個請求時,TCP丟了一個包,TCP自然會重傳,重傳時間是T1,重傳成功後,瀏覽器才能獲取到完整頁面的響應內容,然後渲染和展示整個頁面。也就是說整個頁面的載入時間延遲了T1時間。在此之前,用戶沒有得到任何內容。

但如果建立了100個TCP連接呢?第99個請求出現丟包,那也隻影響了第99個資源的展現,前面接收到的98個資源依然能正常載入,不會導致整個頁面無法載入。

這個問題由於TCP的實現機制,無法解決。這也是HTTP2協議後續要面對的最大挑戰。

當然,who care,google已經在使用了QUIC,就不多介紹了。


題主也就在知乎上問問,我早幾年可是認認真真圖文並茂地認為減少HTTP請求沒用的,你看:對減少HTTP請求的疑問

不過現在能更正確更理智地去看待這些事了

3次握手由於有keep-alive,一條和一百條都只需一次TCP握手--無差別;

你的論斷最大的問題是這句,事實上不是這樣的

當你有100個資源時,這100個資源在瀏覽器看來是「同時都要」,而瀏覽器並沒有什麼智商去判斷應該用1個鏈接解決這100個資源,還是用100個鏈接來解決,不然瀏覽器永遠都只有一個TCP鏈接了

因此瀏覽器的靜態的策略是在自己可承受的範圍內儘可能地用多的鏈接來解決,大部分瀏覽器似乎是6-8個鏈接,這就導致握手也是6-8次

其他的如 @賀師俊 老師說的,網路延遲以及TCP/IP的速率協商也是很需要考慮的問題

補充說下網路延遲這東西,很多做前端的人根本不明白這一點,他們在計算傳輸時間的時候只會考慮到帶寬這一因素,卻絲毫不考慮延遲。我就問你一個問題:

一個200M帶寬、2000ms延遲的網路,和一個2M帶寬,20ms延遲的網路,你喜歡哪個

實際上一個資源從客戶端發出請求到客戶端實際能使用的時間,排除掉dns、握手、伺服器計算等,純粹資源本身和網路相關的計算應該是:

網路延遲 * 2 + 資源大小 / 帶寬

從這個公式可以看出來,當資源大小很小時,網路延遲對性能的影響會很誇張。假設你的資源大小能在一個TCP包中傳輸,那你基本就看網路延遲的臉色決定網路性能了

你把100個資源不合併,就意味著要享受100倍網路延遲,而不合資的資源通常更小,導致網路延遲在每一次網路Roundtrip里佔比更重,會很痛苦的

因此,你的「只是增多了http報文頭」這個推斷雖然正確,但其影響遠不如你想像的那麼小,報頭本身不大,加上gzip後更小,但是抗不住網路延遲的影響,特別是3G網路環境下會更嚴重

當然合併文件也是有一個限制的,這就是為啥我們在下載大文件的時候還是喜歡多線程下載,一般我肯定不會讓一個資源超過1M,再大就要考慮怎麼切分


大嬸們分析的都很深入,只想說一點,盡信書不如無書,不是說書里說的不一定是對的,而是說即使他說的是對的,那也可能只是一個場景下。

例如 雅虎那個多少多少條法則,制定的時候是上個時代了,那時候cdn還是個很稀有的東西,所以裡面的一些法則,拿在現在來看,都不太適用了。

所以,你要理解的不是記住法則,而是理解為什麼會有這條法則!

例如合併請求這條。合併請求是為了什麼?減少了哪些時間?優化了哪些策略?權衡了什麼利弊?合併請求並不是萬能的(如果是,我們的htmljscssimage都放在一個請求里多好)

優點如雲龍大大所說,減少了請求帶來的http層面的一些時間。但是瀏覽器又可以並行載入,如果多個一起載入,有可能反而更節省時間,but!瀏覽器的並行載入又受限於帶寬!你機器帶寬不夠,並行再多也沒用,but!對於小文件來說,其實帶寬影響很小,但是並行的確會比串列更提前完成,but!這兩個時間到底誰長誰短!but!你到底合併了多少個文件,並行了多少個文件,每個文件多大,其實都有影響!but!如果你的伺服器不是cdn是什麼樣的情況,如果是cdn又是什麼情況,是否合併對伺服器的性能有什麼影響?

了解了這些,其實最後合併與否已經不重要了,因為場景不同方案不一,另外節省了那點時間值得么?然後還要考慮從團隊的工程化來看,哪個方案更合適,其實有時候為了團隊開發會犧牲很多東西,不是說犧牲了就是low,別人看著low可能只是片面的思考問題。

恩,其實就是 「片面」 這個詞,多思考點為什麼,片面這個詞就會越來越遠了。


問題沒那麼簡單,首先,瀏覽器是有多連接並發進行訪問的,同一個域名下一般是2-8個並發連接。 除了合併外,Domain Hash 也是常見的優化手段來著的。 所以說,靜態資源並不一定是都放在同一個域名下的。

這樣會首先導致三次握手要發生多次,其次是每個連接都要經歷一次 TCP 慢速啟動。 但比起讓瀏覽器多開帶來的吞吐提升而言,這些倒都是小事了。

舉例的單一連接在真實場景下,實際是在2-8個連接上發生的,純網路部分確實如題所述,在 DNS 上、握手、伺服器解析( 合併絕大部分時候是用在靜態資源如 js/css 上的,通常連 upstream 開銷都沒有)上基本沒有太大區別。 但發送報文、響應內容上都是有很大區別的。

首先,在發送報文上,99次請求頭一般在5k-10k 左右(和主站同域名也就是沒做 cookie-less domain 的話更悲催,如果 cookie 里有512位元組,那光 cookie 就50k 了),由於上下行不對稱,在 pc 上一般至少要100-250ms 左右才能傳完(國內普遍的 512k 上行),移動網路下這個數字分分鐘可能是10-15倍以上(不要忘記移動網路丟包和延遲都要更重),也就是說達到了1-3秒左右。 含512Byte Cookie 的惡劣情況下甚至可能導致增加10秒+的延遲。

99+的頭信息在下行帶寬上的性能損耗大大低於上行,但還是有一些的,而且由於是同一個連接內發生,也並不會有多連接時慢速啟動所帶來的性能損耗。 然而這裡面造成的最大損耗是全程會出現更多的小包、等待和調度,即 RTT 問題被放大許多倍。

在合併請求的前提下,客戶端發包後,服務端會持續傳輸,在寬頻下,慢啟動調優到合理區間後一次分組傳輸周期中發送的數據是很客觀的(initcwnd=10,mtu=1492的前提下都要發送14.5k的數據,慢啟動完成後一般至少30k+/s的數據是很可能的)。 然而,我們知道大部分的 css/js 都是小於這個尺寸的,這也就是說我們無法充分利用其傳輸優勢,必須在串列的完成上一個請求的前提下才能啟動下一個請求的傳輸。 即,一個傳輸周期明明能發送30k+數據給客戶端,卻可能2-10k 後就結束,首先要確保所有 ACK 已收到,然後再開始等待客戶端發來下一個請求,這也就導致不能充分的利用帶寬。 合併的前提下,一個分組周期內收到數十個甚至上百個報文,只需要對最後一個序號的報文做 ACK 就可以,而獨立發送則要為了一次次中斷導致分組數的增多多發很多 ACK。

別忘了,上面說的是寬頻的情況,移動的情況下必然更糟。 另外,這也就是為什麼多開能有效提升吞吐了,因為多請求復用單連接確實很多時候是不能充分利用帶寬的。

該合併的時候還是合併一下吧。


如果是通知類的消息,整個報文本身有意義的數據不到10bit,而一來一去的head卻遠比消息本身多的多,如果是在移動互聯網下,網路情況會更加糟糕。


其實要看情況的。像我們這種伺服器比較多的情況,其實不合併反而比合併更快。

因為瀏覽器不是只會開一條TCP鏈接的,我們可以利用瀏覽器的並發同時從多個伺服器返迴文件。

但是在你只有一個伺服器的情況下,瀏覽器也會看情況用2~3條TCP連接,不是說你的100條連接請求就真的只走一個Keep-Alive的連接了。

而且這裡還有CDN、各地運營商奇怪的網路優化。

所以我覺得還是部署上方案測試更靠譜。慢慢的選出適合自己網站的方式。


一個完整的RTT的時間會受到響應數據大小的影響,如果時間大於keep-alive的timeout時間,那100個請求是否就不是題主說的那樣了?個人愚見,有錯請指出。


區別比你已經意識到的還要多。

儘管不需要額外的建立連接的時間,keep-alive仍然是許多次來回,也就是至少你耗費了請求從client抵達server上的時間。這時間取決於許多因素,但是再優化,光速是無法優化的,假設單程需要50ms(中國本土到美國本土無論從哪個方向跑都至少要這點時間),你就需要浪費5s。

可能還有很多其他差別。這也是為什麼我們需要有HTTP2去改進這些問題。


每個請求再少都有個頭信息,哪怕設置了expires,也要向伺服器發請求,雖然只有很少的位元組,不過積少成多,一年能節省不少流量吧。

不過我有個困惑,不知道gzip在壓縮多個請求和一次性把些請求都壓了,損耗上會不會有差別。


  1. TCP建立連接的時候,需要協商速度,一旦鏈接超時或者重連,會重新slide整個window的大小,這個時候就不是說你想快就快的了。路由上的包不一定會100%到達。

  2. 其次是,瀏覽器一次只會允許少數鏈接,大概是8個左右。
  3. 第三是,你的路由器或交換機不一定每次都把你的http包的優先順序放在第一,尤其是某些QoS演算法爛的路由器,你的包可能經常因為queue滿了而被drop掉。即便你是keep alive,你也沒辦法解決這個問題。
  4. 網路延時比你想像的要大得多。假定你有20ms的延時,100個鏈接,而瀏覽器只給你6個鏈接,你就需要100/6*20 ms。現在html5遊戲用的altas方式也無法讓圖片的傳輸變得足夠快,何況一幅幅圖片來傳?


用事實說話:我在移動端把js/css全用inline的方式打在頁面里,和html共用一個請求,2G下速度提升了100%以上!

用好搜,特順手


我不是WEB前端工作者,只從網路性能角度談談如何理解這個問題,不見得全面正確。

如果把用戶要感知的內容信息比作一堆貨物,用戶使用的網路是條四車道高速公路,其長度就是時延,那麼TCP和HTTP機制共同作用的效果,可以想像成是一支車隊。TCP吞吐量和時延、丟包率也有關係,此處不展開講。

假設用戶的終端和內容伺服器都不是瓶頸,那麼網路性能如何讓用戶獲得最佳體驗的感受,就要取決於車隊怎麼組織,去在最短的時間內把貨物運完了。

TCP是有慢啟動和擁塞避免試探過程的,也就是說,一輛車裝好了貨物,要起步,要給油,要換檔,要經過一段時間,才能把高速公路的限速,也就是用戶家寬頻的上限跑滿,充分利用如果車隊的每輛車每次只裝一個小包裹,那就要發很多次車才能把信息運完,哪怕是四車道並排(瀏覽器多連接並發),效果也不會改善太多。

如果要改變貨物的打包方式,合併內容組織的文件,合併HTTP請求,變小包裹為集裝箱呢?那就不一樣了,車隊可能跑兩趟就把全部信息運完了。用戶一看,哇,好快,秒現!

這就是為什麼下載大文件的平均速率,要比下載小文件更容易接近帶寬上限了。我們在看在線視頻的時候,網卡速率曲線更可能是一個高度等於帶寬的方波,而打開網頁的時候速率曲線往往是一個小尖波,高度離上限還有距離。

實際上由於國內網路CDN和Cache的廣泛存在,以及WEB的頁面多鏈接文件組織形式再優化,也很難像視頻那樣可以以大到一定程度文件的形式存在。一個頁面的元素可能是分布在很多個甚至不同省市CDN伺服器上,請求的目的地址都不一樣,合併HTTP請求帶來的感知優化提升,有時並未完全達到設計者初衷的預期效果,這是因為你的信息貨物放在不同地方的多個倉庫里,那是多條高速公路到你家門口再匯聚成四車道,性能的提升這時候就要靠瀏覽器的多進程設計了。


請題主參考CSS sprites的優化效果


可還記得大明湖畔的big pipe嗎?


大多數情況下好 但是沒有絕對的情況 這個問題說清楚需要很大篇幅 當前基本比較權威的書都只是大概介紹一下。 既然如此,不如從問題本身出發,首先對於性能提升這個性能到底是什麼,要有明確的定義和場景,不同的場景下,會有截然不同的答案. 通常可以根據內容特性,協議特性,系統負荷,能耗效率,網路拓撲,網路容量等等一系列的角度去細化這個問題。

相信這個問題最終的答案是開放式的,這也是各類網路協議在實際應用中一個共同的特點,即提供可配置或自適應參數,根據所應用場景進行自動或手動調配。


推薦閱讀:

國內有哪些互聯網公司已經開始使用 HTTP/2 了?
如何使用Nginx轉發非80埠的非HTTP請求?
什麼是HTTP隧道,怎麼理解HTTP隧道呢?
基於 HTTP 連接下 token 安全問題?
如何讓html img標籤發送的http請求附加某個http header?

TAG:前端開發 | HTTP | 前端優化 | 前端性能 |