跳轉到

Head-of-Line Blocking

一個簡單的定義是:

==當單個(慢)對象阻止其他/後續的對象前進時==

現實生活中一個很好的比喻就是只有一個收銀台的雜貨店。一個顧客買了很多東西,最後會耽誤排在他後面的人,因為顧客是以先進先出(First In, First Out)的方式服務的。另一個例子是只有單行道的高速公路。在這條路上發生一起車禍,可能會使整個通道堵塞很長一段時間。因此,即使是在“頭部(head)”一個單一的問題可以“阻塞(block)”整條“線(line)”。

這個概念一直是最難解決的 Web 性能問題之一。為了理解這一點,讓我們從 HTTP/1.1 開始講起。

HTTP/1.1 的隊頭阻塞

HTTP/1.1 是基於文本的。如下圖 1 所示:

img

圖 1:服務器 HTTP/1.1 響應 script.js

在本例中,瀏覽器基於 HTTP/1.1 上請求簡單的script.js文件(綠色),圖 1 顯示了服務器對該請求的響應。我們可以看到 HTTP 方面本身很簡單:它只是在明文文件內容或“有效荷載”(payload)前面直接添加一些文本“headers”(紅色)。然後,頭(Headers)+ 有效荷載(payload)被傳遞到底層 TCP(橙色),以便真實傳輸到客戶端。對於這個例子,假設我們不能將整個文件放入一個 TCP 包中,並且必須將它分成兩部分。

現在讓我們看看當瀏覽器也請求style.css時發生了什麼,如圖 2:

img

圖 2:服務器 HTTP/1.1 響應 script.js 和 sytle.css

在本例中,當script.js的響應傳輸之後,我們發送style.css(紫色)。style.css的頭部(headers)和內容只是附加在 JavaScript(JS)文件之後。接收者使用Content-Length header 來知道每個響應的結束位置和另一個響應的開始位置(在我們的簡化示例中,script.js是 1000 字節,而style.css只有 600 字節)。

在這個包含兩個小文件的簡單示例中,所有這些似乎都很合理。但是,假設 JS 文件比 CSS 大得多(比如說 1MB 而不是 1KB)。這種情況下,在下載整個 JS 文件之前,CSS 必須等待,儘管它要小得多,其實可以更早地解析/使用。更直接地將其可視化,使用數字 1 表示large_script.js和 2 表示`style.css`,我們會得到這樣的結果:

11111111111111111111111111111111111111122

這就是一個隊頭阻塞問題的例子!現在你可能會想:這很容易解決!只需讓瀏覽器在 JS 文件之前請求 CSS 文件!然而,至關重要的是,瀏覽器無法預先知道這兩個文件中的哪一個在請求時會成為更大的文件。這是因為沒有辦法在 HTML 中指明文件有多大。

為了緩解這個問題,瀏覽器會為 HTTP/1.1 上的每個頁面==開啟多個並行 TCP 連接==。目的就是讓請求可以分佈在這些單獨的連接上,以避免隊頭阻塞。

但是在有大量檔案的情況下,並行 TCP 連接不是個好方法,因為建立大量的 TCP 連接會讓伺服器消耗大量的記憶體。

HTTP/2 如何解決隊頭阻塞

這個問題的“真正”解決方案是採用多路復用(multiplexing)。如果我們可以將每個文件的有效荷載(header)分成更小的片(pieces)或“塊”(chunks),我們就可以在網絡上混合或“交錯”(interleave)這些塊:為 JS 發送一個塊,為 CSS 發送一個塊,然後再發送另一個用於 JS,等等,直到文件被下載為止。使用這種方法,較小的 CSS 文件將更早地下載(並且可用),同時只將較大的 JS 文件延遲一點。用數字形象化我們會得到:

12121111111111111111111111111111111111111

因此,HTTP/2 的目標非常明確:==回到單個 TCP 連接,解決隊頭阻塞問題==。換一種說法:我們希望能夠正確地複用資源塊(resource chunks)。

HTTP/2 非常優雅地解決了這一問題,它在資源塊之前添加了幀(frames)。如圖 4 所示:

img

圖 4:HTTP/1.1 vs HTTP/2 響應 script.js

HTTP/2 在每個塊前面放置一個所謂的資料幀(DATA frame)。這些資料幀主要包含兩個關鍵的元資料。首先:下面的塊屬於哪個資源。每個資源的“字節流(bytestream)”都被分配了一個唯一的數字,即流 id(stream id)。第二:塊的大小是多少。協議還有許多其他幀類型,圖 5 也顯示了頭部幀(HEADERS frame)。這再次使用流 id(stream id)來指出這些頭(headers)屬於哪個響應,這樣甚至可以將頭(headers)從它們的實際響應資料中分離出來。

使用這些幀,HTTP/2 確實允許在一個連接上正確地複用多個資源,參見圖 5:

img

圖 5:HTTP/2 多路復用響應 script.js 和 style.css

與圖 3 中的示例不同,瀏覽器現在可以完美地處理這種情況。它首先處理script.js的頭部幀(HEADERS frame),然後是第一個 JS 塊的資料幀(DATA frame)。從資料幀(DATA frame)中包含的塊長度來看,瀏覽器知道它只延伸到 TCP 資料包 1 的末尾,並且需要從 TCP 資料包 2 開始尋找一個全新的幀。在那裡它確實找到了style.css的頭(HEADERS), 下一個資料幀(DATA frame)含有與第一個資料幀(1)不同的流 id(2),因此瀏覽器知道這屬於不同的資源。同樣的情況也適用於 TCP 資料包 3,其中資料幀(DATA frame)流 id 用於將響應塊“解復用”(de-multiplex)到正確的資源“流”(streams)。

因此,通過“framing”單個消息,HTTP/2 比 HTTP/1.1 更加靈活。它允許在單個 TCP 連接上通過交錯排列塊來多路傳輸多個資源。它還解決了第一個資源緩慢時的隊頭阻塞問題:而不必等待查詢資料庫生成的index.html,服務器可以在等待index.html時開始發送其他資源。

HTTP/2 的方式一個重要結果是,我們突然需要一種方法讓瀏覽器與服務器通信,單個連接的帶寬如何跨資源分佈(distributed)。換一種說法:資源塊應該如何“調度(scheduled)”或交錯(interleaved)。如果我們再次用 1 和 2 來表示,我們會發現對於 HTTP/1.1,唯一的選項是 11112222(我們稱之為順序的(sequential))。然而, HTTP/2 有更多的自由:

  • 公平多路復用(例如兩個漸進的 JPEGs):12121212
  • 加權多路復用(2 是 1 的兩倍):22122122121
  • 反向順序調度(例如 2 是密鑰服務器推送的資源):22221111
  • 部分調度(流 1 被中止且未完整髮送):112222

使用哪種方法是由 HTTP/2 中所謂的“優先級(prioritization)”系統驅動的,選擇的方法對 Web 性能有很大的影響。

TCP 隊頭阻塞

HTTP/2 解決了 HTTP 級別的隊頭阻塞,我們可以稱之為“應用層”隊頭阻塞。然而,在典型的網絡模型中,還需要考慮下面的其他層。可以在圖 6 中清楚地看到這一點:

img

圖 6:典型網絡模型中的幾個協議層

然而,如果我們想將多個 HTTP/2 資源多路傳輸到一個 TCP 連接上,將會產生重要的後果。如圖 7:

img

圖 7:HTTP/2 和 TCP 在透視圖上的差異

雖然我們和瀏覽器都知道我們正在獲取 JavaScript 和 CSS 文件,但 HTTP/2 不需要知道這一點。它只知道它在使用來自不同資源流 id (stream id)的塊。==TCP 甚至不知道它在傳輸 HTTP!== TCP 所知道的就是它被賦予了一系列字節。它使用特定最大大小(maximum size)的資料包,通常大約為 1450 字節。每個資料包只跟踪它攜帶的資料的那一部分(字節範圍),這樣原始資料就可以按照正確的順序重建。

換言之,這兩個層之間的透視圖是不匹配的:HTTP/2 可以看到多個獨立的資源字節流,而 TCP 只看到一個不透明的字節流。圖 7 的 TCP 資料包 3 就是一個例子:TCP 只知道它正在傳輸的任何內容的字節 750 到字節 1599。另一方面,HTTP/2 知道資料包 3 中實際上有兩個獨立資源的兩個塊。

如果 TCP 資料包 2 在網絡中丟失,但資料包 1 和資料包 3 已經到達,會發生什麼情況?

它知道資料包 1 的內容可以安全使用,並將這些內容傳遞給瀏覽器。然而,它發現資料包 1 中的字節和資料包 3 中的字節(放資料包 2 的地方)之間存在間隙,因此還不能將資料包 3 傳遞給瀏覽器。TCP 將資料包 3 保存在其接收緩衝區(receive buffer)中,直到它接收到資料包 2 的重傳副本(這至少需要往返服務器一次),之後它可以按照正確的順序將這兩個資料包都傳遞給瀏覽器。也就是==丟失的資料包 2 隊頭阻塞(HOL blocking)資料包 3!==

TCP 不知道 HTTP/2 的獨立流(streams)這一事實意味著==TCP 層隊頭阻塞(由於丟失或延遲的資料包)也最終導致 HTTP 隊頭阻塞!==

現在,你可能會問:如果我們仍然有 TCP 隊頭阻塞,為什麼還要使用 HTTP/2 呢?

主要原因是雖然資料包丟失確實發生在網絡上,但還是比較少見的。特別是在有線網絡中,包丟失率只有 0.01%。即使是在最差的蜂窩網絡上,在現實中,也很少看到丟包率高於 2%。

此外,TCP 隊頭阻塞對 Web 性能的影響要比 HTTP/1.1 隊頭阻塞小得多,HTTP/1.1 隊頭阻塞幾乎可以保證每次都會遇到它!

然而,在某些情況下,單個連接上的 HTTP/2 很難比 6 個連接上的 HTTP/1.1 快。這主要是由於 TCP 的 ==擁塞控制(congestion control)機制==。

也有一些情況(特別是在資料包丟失率較高的低速網絡上),6 個連接的 HTTP/1.1 仍然比一個連接的 HTTP/2 更為出色,這通常是由於 TCP 級別的隊頭阻塞問題造成的。

正是這個事實極大地推動了新的 QUIC 傳輸協議的開發,以取代 TCP。

HTTP/3(基於 QUIC)如何解決隊頭阻塞

如何解決 TCP 的問題?解決方案很簡單:我們==只是需要讓傳輸層知道不同的、獨立的流!== 這樣,如果一個流的資料丟失,傳輸層本身就知道它不需要阻塞其他流。

儘管這個解決方案概念簡單,但在現實中卻很難實現。由於各種原因,不可能改變 TCP 本身使其具有流意識(stream-aware)。選擇的替代方法是以 QUIC 的形式實現一個全新的傳輸層協議。

為了使 QUIC 現實中可以部署在因特網上,它運行在不可靠的 UDP 協議之上。然而,非常重要的是,這並不意味著 QUIC 本身也是不可靠的!在許多方面,QUIC 應該被看作是一個 TCP 2.0。它包括 TCP 的所有特性(可靠性、擁塞控制、流量控制、排序等)的最佳版本,以及更多其他特性。

QUIC 還完全集成了 TLS(參見圖 6),並且不允許未加密的連接。因為 QUIC 與 TCP 如此不同,這也意味著我們不能僅僅在其上運行 HTTP/2,這就是為什麼創建了 HTTP/3 如圖 8 所示:

img

圖 8:HTTP/1.1 vs HTTP/2 vs HTTP/3 響應 script.js

我們觀察到,讓 QUIC 知道不同的流(streams)是非常簡單的。QUIC 受到 HTTP/2 幀方式(framing-approach)的啟發,還添加了自己的幀(frames);在本例中是流幀(STREAM frame)。流 id(stream id)以前在 HTTP/2 的資料幀(DATA frame)中,現在被下移到傳輸層的 QUIC 流幀(STREAM frame)中。這也說明瞭如果我們想使用 QUIC,我們需要一個新版本的 HTTP 的原因之一:如果我們只在 QUIC 之上運行 HTTP/2,那麼我們將有兩個(可能衝突的)“流層”( stream layers)。相反,HTTP/3 從 HTTP 層刪除了流的概念(它的資料幀(DATA frames)沒有流 id),而是重新使用底層的 QUIC 流。

注意:這並不意味著 QUIC 突然知道 JS 或 CSS 文件,甚至知道它正在傳輸 HTTP;和 TCP 一樣,QUIC 應該是一個通用的、可重用的協議。它只知道有獨立的流(streams),它可以單獨處理,而不必知道它們到底是什麼。

現在我們了解了 QUIC 的流幀(STREAM frames),也很容易看出它們如何幫助解決圖 9 中的傳輸層隊頭阻塞:

img

圖 9:TCP 和 QUIC 在透視圖上的差異

與 HTTP/2 的資料幀(DATA frames)非常相似,QUIC 的流幀(STREAM frames)分別跟踪每個流的字節範圍。這與 TCP 不同,TCP 只是將所有流資料附加到一個大 blob 中。像以前一樣,讓我們考慮一下如果 QUIC 資料包 2 丟失,而 1 和 3 到達會發生什麼。與 TCP 類似,資料包 1 中流 1(stream 1)的資料可以直接傳遞到瀏覽器。然而,對於資料包 3,QUIC 可以比 TCP 更聰明。它查看流 1 的字節範圍,發現這個流幀(STREAM frame)完全遵循流 id 1 的第一個流幀 STREAM frame(字節 450 跟在字節 449 之後,因此資料中沒有字節間隙)。它可以立即將這些資料提供給瀏覽器進行處理。然而,對於流 id 2,QUIC 確實看到了一個缺口(它還沒有接收到字節 0-299,這些字節在丟失的 QUIC 資料包 2 中)。它將保存該流幀(STREAM frame),直到 QUIC 資料包 2 的重傳(retransmission)到達。再次將其與 TCP 進行對比,後者也將資料流 1 的資料保留在資料包 3 中!

類似的情況發生在另一種情形下,資料包 1 丟失,但 2 和 3 到達。QUIC 知道它已經接收到流 2(stream 2)的所有預期資料,並將其傳遞給瀏覽器,只保留流 1(stream 1)。我們可以看到,對於這個例子,QUIC 確實解決了 TCP 的隊頭阻塞!

不過,這種方式有幾個重要的後果。最有影響的是QUIC 資料可能不再以與發送時完全相同的順序發送到瀏覽器。對於 TCP,如果發送資料包 1、2 和 3,它們的內容將以完全相同的順序發送到瀏覽器(這就是導致隊頭阻塞的第一個原因)。然而,對於 QUIC,在上面的第二個示例中,在資料包 1 丟失的情況下,瀏覽器首先看到資料包 2 的內容,然後是資料包 3 的最後一部分,然後是資料包 1 的(重傳),然後是資料包 3 的第一部分。換言之:QUIC 在單個資源流中保留了順序,但不再跨單個流(individual streams)進行排序

這是需要 HTTP/3 的第二個也是最重要的原因,因為事實證明,HTTP/2 中的幾個系統非常嚴重地依賴於 TCP 跨流(across streams)的完全確定性排序。例如,HTTP/2 的優先級系統通過傳輸更改樹資料結構(tree data structure )佈局的操作(例如,將資源 5 添加為資源 6 的子級)工作的。如果這些操作應用的順序與發送順序不同(現在通過 QUIC 是可能出現的),客戶端和服務端的優先級狀態可能不同。HTTP/2 的頭壓縮系統 HPACK 也會發生類似的情況。理解這裡的細節並不重要,只需要得出結論:要讓這些 HTTP/2 系統直接應用 QUIC 是非常困難的。因此,對於 HTTP/3,有些系統使用完全不同的方法。例如,QPACK 是 HTTP/3 的 HPACK 版本,它允許在潛在的隊頭阻塞和壓縮性能之間進行自我選擇的權衡。HTTP/2 的優先級系統甚至被完全刪除,很可能會被HTTP/3 的簡化版本所取代。

所有這些都是因為,與 TCP 不同,QUIC 不能完全保證首先發送的資料也會首先被接收。

所以,所有 QUIC 和重新設想 HTTP 版本的這些工作都是為了消除傳輸層隊頭阻塞。

QUIC 和 HTTP/3 真的完全消除了隊頭阻塞?

如果你有一個 JavaScript 文件,該文件需要重新組裝(re-assembled),就像它是由開發人員創建的一樣(或者,老實說,通過 webpack),否則代碼將無法工作。

這意味著,即使在 QUIC 中,我們仍然有一種隊頭阻塞的形式:如果在單個流中有一個字節間隙,那麼流的後面部分仍然會被阻塞,直到這個間隙被填滿。

這有一個關鍵的含義: ==QUIC 的隊頭阻塞移除只有在多個資源流同時活動時才有效。== 這樣,如果其中一個流上有包丟失,其他流仍然可以繼續。這就是我們在上面圖 9 的例子中看到的。然而,如果在某一時刻只有一個流在活動,任何丟包都會影響到這條孤獨的流,我們仍然會被阻塞。

所以,真正的問題是:==我們會經常有多個並發流(simultaneous streams)嗎?==

此外,正如對 HTTP/2 所解釋的,這是可以通過使用適當的資源調度器/多路復用方法來配置的。流 1 和流 2 可以被發送 1122、2121、1221 等,並且瀏覽器可以使用優先級系統指定它希望服務器遵循的方案(對於 HTTP/3 仍然如此)。所以瀏覽器可以說:嘿!我注意到這個連接有嚴重的資料包丟失。我將讓服務器以 121212 模式而不是 111222 向我發送資源。這樣,如果 1 的一個資料包丟失,2 仍然可以繼續工作。

然而,這種模式的問題是,121212 模式(或者類似的)對資源加載性能通常不是最優的。

先前有提到瀏覽器需要接收整個 JS 或 CSS 文件,然後才能實際執行/應用它(雖然有些瀏覽器已經可以開始編譯/解析部分下載的文件,但它們仍然需要等待它們完整後才能實際使用它們)。但是,大量多路復用這些文件的資源塊最終會延遲它們:

現在對於最佳性能,我們有兩個相互衝突的性能優化建議

  1. 從 QUIC 的隊頭阻塞移除中獲利:多路復用發送資源(12121212)
  2. 為了確保瀏覽器能夠盡快處理核心資源:按順序發送資源(11112222)

那麼,哪一個是正確的?或者至少:哪一個應該優先於另一個?這沒有明確的答案,因為資料包丟失模式很難預測

正如我們在上面討論過的,包丟失通常是突發性的和分組的。這意味著我們上面 12121212 的例子已經過於簡化了。圖 10 給出了一個更真實的概述。在這裡,我們假設在下載 2 個流(綠色和紫色)時,我們有一個 8 個丟失包的突發事件:

img

圖 10:HTTP/3 over QUIC 流多路復用對防止隊頭阻塞的影響。每個矩形是一個單獨的 QUIC 包,為一個流傳輸資料。紅叉表示丟失的包。

在圖 10 的頂部第一行中,我們可以看到(通常)對資源加載性能更好的順序情況。在這裡,我們看到 QUIC 對消除隊頭阻塞確實沒有那麼大的幫助:在丟包之後收到的綠包不能被瀏覽器處理,因為它們屬於經歷丟包的同一個流。第二個(紫色)流的資料尚未收到,因此無法處理。

這與中間一行不同,中間一行(偶然!)丟失的 8 個資料包都來自綠色流。這意味著瀏覽器可以處理最後收到的紫色資料包。然而,正如前面所討論的,如果瀏覽器是 JS 或 CSS 文件,如果有更多的紫色資料出現,瀏覽器可能不會從中受益太多。因此,在這裡,我們從 QUIC 的隊頭阻塞移除中獲得了一些好處(因為紫色沒有被綠色阻止),但是可能會犧牲整體資源加載性能(因為多路復用會導致文件稍後完成)。

最下面一行幾乎是最糟糕的情況。8 個丟失的資料包分佈在兩個流中。這意味著這兩個流現在都被隊頭阻塞了:不是因為它們像 TCP 那樣在等待對方,而是因為每個流仍然需要自己排序。

注意:這也是為什麼大多數 QUIC 實現很少同時創建包含來自多個流(streams)的資料包(packets)的原因。如果其中一個資料包丟失,則會立即導致單個資料包中所有流的隊頭阻塞!

因此,我們看到可能存在某種最佳位置(中間一行),在這裡,隊頭阻塞預防和資源加載性能之間的權衡可能是值得的。然而,正如我們所說,丟包模式很難預測。不會總是 8 個資料包。它們不會總是一樣的 8 個資料包。如果我們搞錯了,丟失的資料包只向左移動了一個,我們突然也少了一個紫色的包,這基本上使我們降級到與最下面一行相似的位置…

QUIC 消除隊頭阻塞可能實際上對 Web 性能沒有太大幫助,因為理想情況下,您不希望為了資源加載性能而對許多流進行多路復用。而且,如果你真的想讓它工作得很好,你就必須非常巧妙地調整你的多路復用方式來適應連接類型,因為你絕對不想在包丟失非常低的快速網絡上進行大量的多路復用(因為它們無論如何都不會遭受太多的隊頭阻塞)。

彩蛋:傳輸擁塞控制

傳輸層協議如 TCP 和 QUIC 包括一種稱為擁塞控制(Congestion Control)的機制。擁塞控制器的主要工作是確保網絡不會同時被過多的數據過載。如果沒有緩衝區的話,數據包就會溢出。所以,它通常只發送一點數據(通常是 14KB),看看是否能通過。如果數據到達,接收方將確認發送回發送方。只要所有發送的數據都得到確認,發送方就在每次 RTT 時將其發送速率加倍,直到觀察到丟包事件(這意味著網絡過載(1 位),它需要後退(1 位))。這就是 TCP 連接如何“探測”其可用帶寬。

注:以上描述只是擁塞控制的一種方法。目前,其他方法也越來越流行,其中主要是BBR 算法。BBR 並沒有直接觀察數據包丟失,而是大量考慮 RTT 波動來確定網絡是否過載,這意味著它通常通過探測帶寬來減少數據包丟失。

關鍵是:擁塞控制機制對每個 TCP(和 QUIC)連接都是獨立的!這反過來也會影響到 HTTP 層的 Web 性能。首先,這意味著 HTTP/2 的單個連接最初只發送 14KB。然而,HTTP/1.1 的 6 個連接在它們的第一次傳輸中發送 14KB,大約是 84KB!隨著時間的推移,這將變得複雜,因為每個 HTTP/1.1 連接使用每個 RTT 將其數據加倍。第二,只有在數據包丟失的情況下,連接才會降低其發送速率。對於 HTTP/2 的單個連接,即使是一個包丟失也意味著它將減慢速度(除了導致 TCP 隊頭阻塞之外!)。然而,對於 HTTP/1.1,只有一個連接上的一個包丟失只會減慢這一個連接的速度:其他 5 個連接可以保持正常的發送和增長。

彩蛋:TLS 隊頭阻塞

如上所述,TLS 協議為應用層協議(如 HTTP)提供加密(和其他功能)。它通過將從 HTTP 獲取的資料包裝到 TLS 記錄中,TLS 記錄在概念上類似於 HTTP/2 幀(frames)或 TCP 資料包(packets)。例如,它們在開始時包含一些元資料,以標識記錄的長度。然後,對該記錄及其 HTTP 內容進行加密並傳遞給 TCP 進行傳輸。

就 CPU 使用率而言,加密可能是一項昂貴的操作,因此一次加密一個好資料塊通常是一個好主意,因為這通常更有效。實際上,TLS 可以以高達 16KB 的塊加密資源,這足以填充大約 11 個典型的 TCP 包(give 或 take)。

然而,至關重要的是,TLS 只能對整個記錄進行解密,這就是為什麼會出現 TLS 隊頭阻塞的情況。假設 TLS 記錄分散在 11 個 TCP 包上,最後一個 TCP 包丟失。由於 TLS 記錄是不完整的,它不能被解密,因此被卡在等待最後一個 TCP 包的重傳。注意,在這個特定的情況下,沒有 TCP 隊頭阻塞:在編號 11 之後沒有資料包被卡住等待重新傳輸。換言之,如果我們在本例中使用純 HTTP 而不是 HTTPS,那麼前 10 個包中的 HTTP 資料可能已經被移動到瀏覽器中進行處理。然而,因為我們需要整個 11 包的 TLS 記錄才能解密它,所以我們有了一種新形式的隊頭阻塞。

雖然這是一個非常具體的情況,在現實中可能不會經常發生,但在設計 QUIC 協議時,它仍然被考慮在內。因為那裡的目標是徹底消除所有形式的隊頭阻塞(或至少盡可能多地消除),甚至這種邊緣情況也必須被移除。這就是為什麼 QUIC 集成了 TLS,它總是以每個包為基礎加密資料,並且不直接使用 TLS 記錄。正如我們所看到的,與使用更大的塊相比,這效率更低,需要更多的 CPU,這也是為什麼 QUIC 在當前實現中仍然比 TCP 慢的主要原因之一

彩蛋:多路復用是否重要?

如果你有兩個文件,你通常最好發送 11112222 而不是 12121212。對於需要在應用之前完全接收的資源,如 JS、CSS 和字體,尤其如此。

如果是這樣的話,我們可能會想為什麼我們需要多路復用?因為多路復用是 HTTP/1.1 沒有的主要特性之一。首先,一些可以增量處理/呈現的文件確實從多路復用中獲益。例如,漸進式圖像就是這樣。第二,如上所述,如果其中一個文件比其他文件小得多,那麼它可能會很有用,因為它將更早地下載,而不會對其他文件造成太多的延遲。第三,多路復用允許改變響應的順序,並為高優先級的響應中斷低優先級的響應

現實中出現的一個很好的例子是在源服務器前面使用 CDN 緩存。假設瀏覽器從 CDN 請求兩個文件。第一個(1)沒有被緩存,需要從源文件中獲取,這需要一段時間。第二個資源(2)緩存在 CDN 中,因此可以直接傳輸回瀏覽器。

Reference