本文討論的場景基於HTTP 1.1, 不涉及HTTP 2。
面試時,經常會問候選人一個問題:如何提高網頁效能?
有些基礎的人都會提到這麼一條:減少/合併HTTP請求。
繼續問:瀏覽器不是可以並行下載資源嗎?將多個資源合併成一個資源,只使用一個HTTP請求下載,難道要比用多個HTTP請求並行下載沒有合併過的多個資源速度更快?
候選人:……(至今,還沒有遇到讓我滿意的回答)
減少HTTP請求,是雅虎前端效能優化35條軍規的第1條,2006年雅虎提出了這35條軍規,從那以後,就深深地影響到了一批又一批的前端開發者,即使在12年後的今天,影響力依舊不減…..
但是,雅虎軍規中還有1條是:拆分資源以最大化利用瀏覽器並行下載的能力。現在問題就來了,減少HTTP請求,但網頁所需的資源並不能減少(否則網頁就不再是之前的網頁了),所以減少HTTP請求,主要是通過合併資源來實現的,一邊是建議合併資源,一邊是建議拆分資源,顯然是有衝突的地方,那麼到底該怎麼做呢?網上有些文章也討論過這個問題,但大多是停留在想當然的理論分析上,而且忽略了TCP傳輸機制的影響。今天,老幹部就帶大家一起用實驗+理論,仔細探討下這個問題。
HTTP請求過程
一個HTTP請求的主要過程是:
DNS解析(T1) -> 建立TCP連線(T2) -> 傳送請求(T3) -> 等待伺服器返回首位元組(TTFB)(T4) -> 接收資料(T5)。
如下圖所示,是Chrome Devtools中顯示的一個HTTP請求,顯示了HTTP請求的主要階段,注意,Queueing階段是請求在瀏覽器佇列中的排隊時間,並不計入HTTP請求時間。
從這個過程中,可以看出如果合併N個HTTP請求為1個,可以節省(N-1)* (T1+T2+T3+T4) 的時間。
但實際場景並沒有這麼理想,上面的分析存在幾個漏洞:
- 瀏覽器會快取DNS資訊,因此不是每次請求都需要DNS解析。
- HTTP 1.1 keep-alive的特性,使HTTP請求可以複用已有TCP連線,所以並不是每個HTTP請求都需要建立新的TCP連線。
- 瀏覽器可以並行傳送多個HTTP請求,同樣可能影響到資源的下載時間,而上面的分析顯然只是基於同一時刻只有1個HTTP請求的場景。
實驗論證
我們來做4組實驗,對比一個HTTP請求載入合併後的資源所需時間,和多個HTTP請求並行載入拆分的資源所需時間。每組實驗所用資源的體積大小有顯著差異。
實驗環境:
伺服器:阿里雲ECS 1核 2GB記憶體 頻寬1M
Web伺服器:Nginx (未啟用Gzip)
Chrome v66 隱身模式,禁用快取
Client 網路:wifi 頻寬20M
實驗 1
測試檔案:large1.css、large2.css … large6.css,每個檔案141K;large-6in1.css,由前面6個css檔案合併而成,大小為846K。parallel-large.html引用large1.css、large2.css … large6.css, combined-large.html引用large-6in1.css,程式碼如下:
// parallel-large.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Parallel Large</title>
<link rel="stylesheet" type="text/css" media="screen" href="large1.css" />
<link rel="stylesheet" type="text/css" media="screen" href="large2.css" />
<link rel="stylesheet" type="text/css" media="screen" href="large3.css" />
<link rel="stylesheet" type="text/css" media="screen" href="large4.css" />
<link rel="stylesheet" type="text/css" media="screen" href="large5.css" />
<link rel="stylesheet" type="text/css" media="screen" href="large6.css" />
</head>
<body>
Hello, world!
</body>
</html>
複製程式碼
// combined-large.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Combined Large</title>
<link rel="stylesheet" type="text/css" media="screen" href="large-6in1.css" />
</head>
<body>
Hello, world!
</body>
</html>
複製程式碼
分別重新整理2個頁面各10次,利用Devtools 的Network計算CSS資源載入的平均時間。
注意事項:
- large1.css、large2.css … large6.css的載入時間,計算方式為從第一個資源的HTTP請求傳送開始,到6個檔案都下載完成的時間,如圖2紅色框內的時間。
- 兩個html頁面不能同時載入,否則頻寬為兩個頁面所共享,會影響測試結果。需要等待一個頁面載入完畢後,再手動重新整理載入另外一個頁面。
- 頁面兩次重新整理時間間隔在1分鐘以上 ,以避免HTTP 1.1 連線複用對實驗的影響。
實驗結果如下:
large-6in1.css | large1.css、large2.css … large6.css | |
---|---|---|
平均時間(s) | 5.52 | 5.3 |
我們再把large1.css、large2.css … large6.css合併為3個資源large-2in1a.css、large-2in1b.css、large-2in1c.css,每個資源282K,在combined-large-1.html中引用這3個資源:
// combined-large-1.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Parallel Large 1</title>
<link rel="stylesheet" type="text/css" media="screen" href="large-2in1a.css" />
<link rel="stylesheet" type="text/css" media="screen" href="large-2in1b.css" />
<link rel="stylesheet" type="text/css" media="screen" href="large-2in1c.css" />
</head>
<body>
Hello, world!
</body>
</html>
複製程式碼
測試10次,平均載入時間為5.20s。
彙總實驗結果如下:
large-6in1.css | large1.css、large2.css … large6.css | large-2in1a.css、... large-2in1c.css | |
---|---|---|---|
平均時間(s) | 5.52 | 5.30 | 5.20 |
從實驗1結果可以看出,合併資源和拆分資源對於資源的總載入時間沒有顯著影響。實驗中耗時最少的是拆分成3個資源的情況(5.2s),耗時最多的是合併成一個資源的情況(5.52s),但兩者也只不過相差6%。考慮到實驗環境具有一定隨機性,以及實驗重複次數只有10次,這個時間差並不能表徵3種場景有明顯的時間差異性。
實驗 2
繼續增加css檔案大小。
測試檔案:xlarge1.css、xlarge2.css 、xlarge3.css,每個檔案1.7M;xlarge-3in1.css,由前面3個css檔案合併而成,大小為5.1M。parallel-xlarge.html引用xlarge1.css、xlarge2.css 、xlarge3.css, combined-xlarge.html引用xlarge-3in1.css。
測試過程同上,實驗結果如下:
xlarge-3in1.css | xlarge1.css、xlarge2.css、xlarge3.css | |
---|---|---|
平均時間(s) | 37.72 | 36.88 |
這組實驗的時間差只有2%,更小了,所以更無法說明合併資源和拆分資源的總載入時間有明顯差異性。
實際上,理想情況下,隨著資源體積變大,兩種資源載入方式所需時間將趨於相同。
從理論上解釋,因為HTTP的傳輸通道是基於TCP連線的,而TCP連線具有慢啟動的特性,剛開始時並沒有充分利用網路頻寬,經過慢啟動過程後,逐漸佔滿可利用的頻寬。對於大資源而言,頻寬總是會被充分利用的,所以頻寬是瓶頸,即使使用更多的TCP連線,也不能帶來速度的提升。資源越大,慢啟動所佔總的下載時間的比例就越小,絕大部分時間,頻寬都是被充分利用的,總資料量相同(拆分資源導致的額外Header在這種情況下完全可以忽略不計),頻寬相同,傳輸時間當然也相同。
實驗 3
減小css檔案大小。
測試檔案:medium1.css、medium2.css … medium6.css,每個檔案9.4K;medium-6in1.css,由前面6個css檔案合併而成,大小為56.4K。parallel-medium.html引用medium1.css、medium2.css … medium6.css, combined-medium.html 引用 medium-6in1.css。
實驗結果如下:
medium-6in1.css | medium1.css、medium2.css … medium6.css | |
---|---|---|
平均時間(ms) | 34.87 | 46.24 |
注意單位變成ms
實驗3的時間差是33%,雖然數值上只差12ms。先不多分析,繼續看實驗4。
實驗 4
繼續減小css檔案大小,至幾十位元組級別。
測試檔案:small1.css、small2.css … small6.css,每個檔案28B;small-6in1.css,由前面6個css檔案合併而成,大小為173B。parallel-medium.html引用small1.css、small2.css … small6.css, combined-medium.html 引用 small-6in1.css。
實驗結果如下:
small-6in1.css | small1.css、small2.css … small6.css | |
---|---|---|
平均時間(ms) | 20.33 | 35 |
實驗4的時間差是72%。
根據實驗3和實驗4,發現當資源體積很小時,合併資源和拆分資源的載入時間有了比較明顯的差異。圖3和圖4是實驗4中的某次測試結果的截圖,當資源體積很小時,資料的下載時間(圖中水平柱的藍色部分所示)佔總時間的比例就很小了,這時候影響資源載入時間的關鍵就是DNS解析(T1) 、 TCP連線建立(T2) 、傳送請求(T3) 和等待伺服器返回首位元組(TTFB)(T4) 。但同時建立多個HTTP連線本身就存在額外的資源消耗,每個HTTP的DNS查詢時間、TCP連線的建立時間等也存在一定的隨機性,這就導致併發請求資源時,出現某個HTTP耗時明顯增加的可能性變大。如圖3所示,small1.css載入時間最短(16ms),small5.css載入時間最長(32ms),兩者相差了1倍,但計算時間是以所有資源都載入完成為準,這種情況下,同時使用多個HTTP請求就會導致更大的時間不均勻性和不確定性,表現結果就是往往要比使用一個HTTP請求載入合併後的資源慢。
更復雜的情況
對於小檔案一定是合併資源更快嗎?
其實未必,在一些情況下,合併小檔案反而有可能明顯增加資源載入時間。
再說些理論的東西。為了提高傳輸效率,TCP通道上,並不是傳送方每傳送一個資料包,都要等到收到接收方的確認應答(ACK)後,再傳送下一個報文。TCP引入了”視窗“的概念,視窗大小指無需等待確認應答而可以繼續傳送資料的最大值,例如視窗大小是4個MSS(Maximum Segment Size,TCP資料包每次能夠傳輸的最大資料分段),表示當前可以連續傳送4個報文段,而不需要等待接收方的確認訊號,也就是說,在1次網路往返(round-trip)中完成了4個報文段的傳輸。如下圖所示(MSS為1,視窗大小為4個MSS),1 - 4000 資料是連續傳送的,並沒有等待確認應答,同樣的,4001 - 8000也是連續傳送的。請注意,這只是理想情況下的示意圖,實際情況要比這裡更復雜。
在慢啟動階段,TCP維護一個擁塞視窗變數,這個階段視窗的大小就等於擁塞視窗,慢啟動階段,隨著每次網路往返,擁塞視窗的大小就會翻一倍,例如,假設擁塞視窗的初始大小為1,擁塞視窗的大小變化為:1,2,4,8……。如下圖所示。
實際網路中,擁塞視窗的初始值一般是10,所以擁塞視窗的大小變化為:10,20,40 ... ,MSS的值取決於網路拓撲結構和硬體裝置,乙太網中MSS值一般是1460位元組,按每個報文段傳輸的資料大小都等於MSS計算(實際情況可以小於MSS值),經過第1次網路往返後,傳輸的最大資料為14.6K,第2次後,為(10+20) * 1.46 = 43.8K, 第3次後,為(10+20+40) * 1.46 = 102.2K。
根據上面的理論介紹,實驗4中,不管是合併資源,還是拆分資源,都是在1次網路往返中傳輸完成。但實驗3,拆分後的資源大小為9.4K,可以在1次網路往返中傳輸完成,而合併後的資源大小為56.4K,需要3次網路往返才能傳輸完成,如果網路延時很大(例如1s),頻寬又不是瓶頸,多了兩次網路往返將導致耗時增加1s,這時候合併資源就可能得不償失了。實驗3並沒有產生這個結果的原因是,實驗中網路延時是10ms左右,由於數值太小而沒有對結果產生明顯影響。
總結
對於大資源,是否合併對於載入時間沒有明顯影響,但拆分資源可以更好的利用瀏覽器快取,不會因為某個資源的更新導致所有資源快取失效,而資源合併後,任一資源的更新都會導致整體資源的快取失效。另外還可以利用域名分片技術,將資源拆分部署到不同域名下,既可以分散伺服器的壓力,又可以降低網路抖動帶來的影響。
對於小資源,合併資源往往具有更快的載入速度,但在網路頻寬狀況良好的情況下,因為提升的時間單位以ms計量,收益可以忽略。如果網路延遲很大,伺服器響應速度又慢,則可以帶來一定收益,但在高延遲的網路場景下,又要注意合併資源後可能帶來網路往返次數的增加,進而影響到載入時間。
其實,看到這裡,是合是分已經不重要了,重要的是我們要知道合分背後的原理是什麼,和業務場景是怎樣的。
我的新書《React進階之路》已上市,對React感興趣的同學不妨去了解下。 購買地址: 噹噹 京東
歡迎關注我的公眾號:老幹部的大前端