HTTP 請求之合併與拆分技術詳解

你好,我是 TianTian

說起 HTTP 請求,如何做到請求的合併與拆分,怎麼樣優化效果最明顯,你有思考過這個問題麼,這篇文章可能會給你啓示,以下是正文。

作者:darminzhou,騰訊 CSIG 前端開發工程師

導語:HTTP/2 中,是否還需要減少請求數?來看看實驗數據吧。

1. 背景

隨着網站升級 HTTP/2 協議,在瀏覽頁面時常常會發現頁面的請求數量很大,尤其是小圖片請求,經典的雅虎前端性能優化軍規中的第 1 條就是減少請求數,在 HTTP/1.1 時代合併雪碧圖是這種場景減少請求數的一大途徑,但是現在這些圖片是使用 HTTP/2 協議傳輸的,這種方式是否也適用?另外,在都使用 HTTP/2 的情況,在瀏覽器併發這麼多小圖片請求時,是否會影響其他靜態資源的拉取速度(例如頁面 js 文件的請求耗時)?

基於上面問題的思考,本文進行了一個簡單的實驗,嘗試通過數據來分析 HTTP 中的合併與拆分,以及併發請求是否影響其他請求。通過這次的實驗我們對比了以下幾個不同 HTTP 場景的耗時數據:

2. 實驗準備

理論:合併與拆分都是 HTTP 請求優化的常用方法,合併主要爲了減少請求數,可以減少多次建立 TCP 連接耗時,不過相對的,緩存命中率會受到影響;拆分主要爲了利用併發能力,瀏覽器可以併發多個 TCP 連接,還可以結合 HTTP/1.1 中的長鏈接,不過受 HTTP 隊頭阻塞影響,併發能力並不強,於是 HTTP/2 協議出現,使用多路複用、頭部壓縮等技術很好的解決了 HTTP 隊頭阻塞問題,實現了較強的併發能力。而 HTTP/2 由於基於 TCP,依然無法繞過 TCP 隊頭阻塞問題,於是又出現了 HTTP/3,不過本文並不討論 HTTP/3,有興趣的同學可以自行 Google。實驗環境:

爲了避免自己搭服務器可能存在的性能影響,實驗中的圖片資源數據使用騰訊雲的 COS 存儲,並開啓了 CDN 加速。

3. 實驗分析

第一個實驗:有 2 個 HTML。1 個 HTML 中併發加載 361 張小圖片,記錄所有圖片加載完成時的耗時;另 1 個 HTML 加載合併圖並記錄其耗時。並分別記錄基於 HTTP/1.1 和 HTTP/2 協議的不同限速情況的請求耗時情況。每個場景測試 5 次,每次都間隔一段時間避免某一時間段網絡不好造成的數據偏差,最後計算平均耗時。實驗數據:

3.1 HTTP/1.1 合併 VS 拆分

根據上面實驗數據,抽出其中 HTTP/1.1 的合併和拆分的數據來看,很明顯拆分的多個小請求耗時遠大於合併的請求,且網速較低時差距更大。

HTTP/1.1 合併請求的優化原理

簡單看下 HTTP 請求的主要過程:DNS 解析 (T1) -> 建立 TCP 連接 (T2) -> 發送請求 (T3) -> 等待服務器返回首字節(TTFB)(T4) -> 接收數據 (T5)。

從上面請求過程中,可以看出當多個請求時,請求中的 DNS 解析、建立 TCP 連接等步驟都會重複執行多遍。那麼如果合併 N 個 HTTP 請求爲 1 個,理論上可以節省(N-1)* (T1+T2+T3+T4) 的時間。當然實際場景並沒有這麼理想,比如瀏覽器會緩存 DNS 信息,因此不是每次請求都需要 DNS 解析;比如 HTTP/1.1 keep-alive 的特性,使 HTTP 請求可以複用已有 TCP 連接,所以並不是每個 HTTP 請求都需要建立新的 TCP 連接;再比如瀏覽器可以並行發送多個 HTTP 請求,同樣可能影響到資源的下載時間,而上面的分析顯然只是基於同一時刻只有 1 個 HTTP 請求的場景。

感興趣深入瞭解的可以參考網上一篇 HTTP/1.1 詳細實驗數據,其結論是:當文件體積較小的時候,(網絡延遲低的場景下)合併後的文件的加載耗時明顯小於加載多個文件的總耗時;當文件體積較大的時候,合併請求對於加載耗時沒有明顯的影響,且拆分資源可以提高緩存命中率。但是注意有特殊的場景,由於合併資源後可能導致網絡往返次數的增加,當網絡延遲很大時,是會增大耗時的(參考 TCP 擁塞控制)。

【擴展:TCP 擁塞控制】 TCP 中包含一種稱爲擁塞控制的機制,擁塞控制的主要工作是確保網絡不會同時被過多的數據傳輸導致過載。當前擁塞控制的方法有許多,主要原理是慢啓動,例如,開始階段只發送一點數據,觀察是否能通過,如果能接收方將確認發送回發送方,只要所有數據都得到確認,發送方就在下次 RTT 時將發送數據量加倍,直到觀察到丟包事件(丟包意味着過載,需要後退),每次發送的數據量即擁塞窗口,就是這樣動態調整擁塞窗口來避免擁塞。擁塞控制機制對每個 TCP 連接都是獨立的。

3.2 HTTP/1.1 VS HTTP/2 併發請求

抽出實驗數據中的 HTTP/1.1 和 HTTP/2 併發請求來對比分析,可以看出 HTTP/2 的併發總耗時明顯優於 HTTP/1.1,且網速越差,差距越大。

HTTP/2 多路複用和頭部壓縮的原理

多路複用 :在一個 TCP 鏈接中可以並行處理多個 HTTP 請求,主要是通過流和幀實現,一個流代表一個 HTTP 請求,每個 HTTP 資源拆分成一個個的幀按順序進行傳輸,不同流的幀可以穿插傳輸,最終依然能根據流 ID 組合成完整資源,以此實現多路複用。幀的類型有 11 種,例如 headers 幀(請求頭 / 響應頭),data 幀(body),settings 幀(控制傳輸過程的配置信息,例如流的併發上限數、緩衝容量、每幀大小上限)等等。

頭部壓縮 :爲了節約傳輸消耗,通過壓縮的方式傳輸同一個 TCP 鏈接中不同 HTTP 請求 / 響應的頭部數據,主要利用了靜態表和動態表來實現,靜態表規定了常用的一些頭部,只用傳輸一個索引即可表示,動態表用於管理一些頭部數據的緩存,第一次出現的頭部添加至動態表中,下次傳輸同樣的頭部時就只用傳輸一個索引即可。由於基於 TCP,頭部幀的發送和接收後的處理順序是保持一致的,因此兩端維護的動態表也就保證一致。

多路複用允許一次 TCP 鏈接處理多次 HTTP 請求,頭部壓縮又大大減少了多個 HTTP 請求可能產生的重複頭部數據消耗。因此 HTTP/2 可以很好的支持併發請求,感興趣可以深入 HTTP/2 瀏覽器源碼分析。

【擴展:隊頭阻塞】 HTTP/2 解決了 HTTP/1.1 中 HTTP 層面(應用層)隊頭阻塞的問題,但是由於 HTTP/2 仍然基於 TCP,因此 TCP 層面的隊頭阻塞依然存在。HTTP/3 使用 QUIC 解決了 TCP 隊頭阻塞的問題。感興趣可以看看隊頭阻塞這篇文章。

HTTP 層面的隊頭阻塞在於,HTTP/1.1 協議中同一個 TCP 連接中的多個 HTTP 請求只能按順序處理,方式有兩種標準,非管道化和管道化兩種,非管道化方式:即串行執行,請求 1 發送並響應完成後纔會發送請求 2,一但前面的請求卡住,後面的請求就被阻塞了;管道化方式:即請求可以並行發出,但是響應也必須串行返回(只提出過標準,沒有真正應用過)。

TCP 層面的隊頭阻塞在於,TCP 本身不知道傳輸的是 HTTP 請求,TCP 只負責傳遞數據,傳遞數據的過程中會將數據分包,由於網絡本身是不可靠的,TCP 傳輸過程中,當存在數據包丟失的情況時,順序排在丟失的數據包之後的數據包即使先被接收也不會進行處理,只會將其保存在接收緩衝區中,爲了保證分包數據最終能完整拼接成可用數據,所丟失的數據包會被重新發送,待重傳副本被接收之後再按照正確的順序處理它以及它後面的數據包。

But,由於 TCP 的握手協議存在,TCP 相對比較可靠,TCP 層面的丟包現象比較少見,需要明確的是,TCP 隊頭阻塞是真實存在的,但是對 Web 性能的影響比 HTTP 層面隊頭阻塞小得多,因此 HTTP/2 的性能提升還是很有作用的。

HTTP/2 中存在 TCP 的隊頭阻塞問題主要由於 TCP 無法記錄到流 id,因爲如果 TCP 數據包攜帶流 id,所丟失的數據包就只會影響數據包中相關流的數據,不會影響其他流,所以順序在後的其他流數據包被接收到後仍可處理。出於各種原因,無法改造 TCP 本身,因此爲了解決 HTTP/2 中存在的 TCP 對頭阻塞問題,HTTP/3 在傳輸層不再基於 TCP,改爲基於 UDP,在 UDP 數據幀中加入了流 id 信息。

3.3 HTTP/2 合併 VS 拆分

由於 HTTP/2 支持多路複用和頭部壓縮,是不是原來 HTTP/1.1 中的合併請求的優化方式就沒用了,在 HTTP/2 中合併雪碧圖有優化效果嗎?

抽出 HTTP/2 的合併和拆分的數據來看,拆分的多個小請求耗時仍大於合併的請求,不過差距明顯縮小了很多。那麼爲什麼差距還是挺大呢?理論上 HTTP/2 的場景下,帶寬固定,總大小相同的話,拆分的多個請求最好的情況應該是接近合併的總耗時的纔對吧。

分析一下,因爲是複用一個 TCP 連接,所以首先排除重複 DNS 查詢、建立 TCP 連接這些影響因素。那麼再分析一下資源大小的影響:

  1. 本身合併的圖片(516kB)就比拆分的 361 張小圖片總大小(總 646kB)要小。

  2. 拆分的很多個小請求時,雖然有頭部壓縮,但是請求和響應中的頭部數據以及一些 settings 幀數據還是會多一些。通過查看 chrome 的 transferred 可以知道小圖片最終總傳輸數據 741kB,說明除 body 外多傳輸了將近 100kB 的數據。

結合上面兩點,理論上拆分的小圖片總耗時應該是合併圖片的耗時的(741/516=)1.44 倍。但是很明顯測試中各網速場景下拆分的小圖片總耗時與合併圖片耗時的比值都大於 1.44 這個理論值(2.62、2.96、1.84)。其中的原因這裏有兩點推測:

  1. 併發多個請求的總耗時計算的是所有請求加載完的耗時,每個請求都有發送和響應過程,其中分爲一個個幀的傳輸過程,只要其中某小部分發生阻塞,就會拖累總耗時情況。

  2. 瀏覽器在處理併發請求過程存在一定的調度策略而導致。推測的依據來自 Chrome 開發者工具中的 Waterfall,可以看到很多併發請求的 Queueing Time、Stalled Time 很高,說明瀏覽器不會在一開始就並行發送所有請求。

不過這裏只屬於猜測,還未深入探究。

3.4 瀏覽器併發 HTTP/2 請求數(大量 VS 少量)時,其他請求的耗時

第二個實驗:在 HTML 中分別基於 HTTP/2 加載 360 + 張小圖片、130 + 張小圖片、20 + 張小圖片、0 張小圖片,以及 1 張大圖片和 1 個 js 文件,大圖片在 DOM 中放在所有小圖片的後面,圖片都是同域名的,js 文件是不同域名的,然後記錄大圖片和腳本的耗時,同樣也是利用 Chrome 限速工具在不同的網絡限速下測試(不過這個連的 WIFI 與第一個實驗中不同,無限速時的網速略微不同)。這個實驗主要用於分析併發請求過多時是否會影響其他請求的訪問速度。實驗數據:

從實驗數據中可以看出,

  1. 圖片併發數量不會影響 js 的加載速度,無限速時無論併發圖片請求有多少,腳本加載都只要 0.12s 左右。

  2. 很明顯對大圖片的加載速度有影響,可以看到併發量從大到小時,大圖片的耗時明顯一次減少。

但是其中也有幾個反常的數據:Fast3G 和 Slow3G 的網速限制下,無小圖片時的 js 加載耗時明顯高於有併發小圖片請求的 js 加載耗時。這是爲啥?我們推測這裏的原因是,由於圖片和 js 不同域名,分別在兩個 TCP 連接中傳輸,兩個 TCP 是分享總網絡帶寬的,當有多個小圖片時,小圖片在 DOM 前優先級高,js 和小圖片分享網絡帶寬,js 體積較大佔用帶寬較多,而無小圖片時,js 是和大圖片分享網絡帶寬,js 佔用帶寬比率變小,因此在限速時帶寬不夠的情況下表現出這樣的反常數據。

4. 實驗結論

  1. HTTP/1.1 中合併請求帶來的優化效果還是明顯的。

  2. 對於多併發請求的場景 HTTP/2 比 HTTP/1.1 的優勢也是挺明顯的。不過也要結合具體環境,HTTP/2 中由於複用 1 個 TCP 鏈接,如果併發中某一個大請求資源丟包率嚴重,可能影響導致整個 TCP 鏈路的流量窗口一直很小,而這時 HTTP/1.1 中可以開啓多個 TCP 鏈接可能其他資源的加載速度更快?當然這也只是個人猜測,沒有具體實驗過。

  3. HTTP/2 中合併請求耗時依然會比拆分的請求總耗時低一些,但是相對來說效果沒有 HTTP/1.1 那麼明顯,可以多結合其他因素,例如拆分的必要性、緩存命中率需求等,綜合決策是否合併或拆分。

  4. 網速較好的情況下,非同域名下的請求相互間不受影響,同域名的併發請求,隨着併發量增大,優先級低的請求耗時也會增大。

不過,本文中的實驗環境較爲有限,說不定換了一個環境會得到不同的數據和結論?比如不同的瀏覽器(Firefox、IE 等)、不同的操作系統(Windows、Linux 等)、不同的服務端能力以及不同測試資源等等,大家感興趣也可以抽點時間試一試。

5. 其他思考

以上討論主要針對低計算量的靜態資源,那麼高計算量的動態資源的請求呢,(例如涉及鑑權、數據庫查詢之類的),合併 vs. 拆分?

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Mg4yXQGHTAMz6eKXpPV6Ww