HTTP/2 與 WEB 性能優(yōu)化(二)
在「HTTP/2 與 WEB 性能優(yōu)化(一)」這篇博客中,我主要寫(xiě)了 HTTP/2 中的 Server Push 給 WEB 性能優(yōu)化帶來(lái)的便利,今天繼續(xù)來(lái)聊一聊 HTTP/2 其他方面的改變。
我們知道,HTTP/2 并沒(méi)有改動(dòng) HTTP/1 的語(yǔ)義部分,例如請(qǐng)求方法、響應(yīng)狀態(tài)碼、URI 以及頭部字段等核心概念依舊存在。HTTP/2 最大的變化是重新定義了格式化和傳輸數(shù)據(jù)的方式,這是通過(guò)在高層 HTTP API 和低層 TCP 連接中引入二進(jìn)制分幀層來(lái)實(shí)現(xiàn)。這樣改動(dòng)的好處是原來(lái)的 WEB 應(yīng)用完全不用修改,就能享受到協(xié)議升級(jí)帶來(lái)的收益。
HTTP/2 的連接
HTTP/1 的請(qǐng)求和響應(yīng)報(bào)文,都是由起始行、首部和實(shí)體正文(可選)組成,各部分之間以文本換行符分隔。而 HTTP/2 將請(qǐng)求和響應(yīng)數(shù)據(jù)分割為更小的幀,并對(duì)它們采用二進(jìn)制編碼。下面這幅圖中的 Binary Framing 就是新增的二進(jìn)制分幀層:
先來(lái)看看這幾個(gè)概念:
幀(Frame):HTTP/2 數(shù)據(jù)通信的最小單位。幀用來(lái)承載特定類(lèi)型的數(shù)據(jù),如 HTTP 首部、負(fù)荷;或者用來(lái)實(shí)現(xiàn)特定功能,例如打開(kāi)、關(guān)閉流。每個(gè)幀都包含幀首部,其中會(huì)標(biāo)識(shí)出當(dāng)前幀所屬的流;
消息(Message):指 HTTP/2 中邏輯上的 HTTP 消息。例如請(qǐng)求和響應(yīng)等,消息由一個(gè)或多個(gè)幀組成;
流(Stream):存在于連接中的一個(gè)虛擬通道。流可以承載雙向消息,每個(gè)流都有一個(gè)唯一的整數(shù) ID;
連接(Connection):與 HTTP/1 相同,都是指對(duì)應(yīng)的 TCP 連接;
在 HTTP/2 中,同域名下所有通信都在單個(gè)連接上完成,這個(gè)連接可以承載任意數(shù)量的雙向數(shù)據(jù)流。每個(gè)數(shù)據(jù)流都以消息的形式發(fā)送,而消息又由一個(gè)或多個(gè)幀組成。多個(gè)幀之間可以亂序發(fā)送,因?yàn)楦鶕?jù)幀首部的流標(biāo)識(shí)可以重新組裝。下面有一幅圖說(shuō)明幀、消息、流和連接的關(guān)系:
TCP 協(xié)議本身更適合用來(lái)長(zhǎng)時(shí)間傳輸大數(shù)據(jù),這樣它的穩(wěn)定和可靠性才能顯露出來(lái)。HTTP/1 時(shí)代太多短而小的 TCP 連接,反而更多地將 TCP 的缺點(diǎn)給暴露出來(lái)了。
HTTP/1 的連接
在 HTTP/1 中,每一個(gè)請(qǐng)求和響應(yīng)都要占用一個(gè) TCP 連接,盡管有 Keep-Alive 機(jī)制可以復(fù)用,但在每個(gè)連接上同時(shí)只能有一個(gè)請(qǐng)求 / 響應(yīng),這意味著完成響應(yīng)之前,這個(gè)連接不能用于其他請(qǐng)求(怎么判斷響應(yīng)是否結(jié)束,可以看這里)。如果瀏覽器需要向同一個(gè)域名發(fā)送多個(gè)請(qǐng)求,需要在本地維護(hù)一個(gè) FIFO 隊(duì)列,完成一個(gè)再發(fā)送下一個(gè)。這樣,從服務(wù)端完成請(qǐng)求開(kāi)始回傳,到收到下一個(gè)請(qǐng)求之間的這段時(shí)間,服務(wù)端處于空閑狀態(tài)。
后來(lái),人們提出了 HTTP 管道(HTTP Pipelining)的概念,試圖把本地的 FIFO 隊(duì)列挪到服務(wù)端。它的原理是這樣的:瀏覽器一股腦把請(qǐng)求都發(fā)給服務(wù)端,然后等著就可以了。這樣服務(wù)端就可以在處理完一個(gè)請(qǐng)求后,馬上處理下一個(gè),不會(huì)有空閑了。甚至服務(wù)端還可以利用多線程并行處理多個(gè)請(qǐng)求??上?,因?yàn)? HTTP/1 不支持多路復(fù)用,這個(gè)方案有幾個(gè)棘手的問(wèn)題:
服務(wù)端收到多個(gè)管道請(qǐng)求后,需要按接收順序逐個(gè)響應(yīng)。如果恰好第一個(gè)請(qǐng)求特別慢,后續(xù)所有響應(yīng)都會(huì)跟著被阻塞。這種情況通常被稱(chēng)之為「隊(duì)首阻塞(Head-of-Line Blocking)」;
服務(wù)端為了保證按順序回傳,通常需要緩存多個(gè)響應(yīng),從而占用更多的服務(wù)端資源,也更容易被人攻擊;
瀏覽器連續(xù)發(fā)送多個(gè)請(qǐng)求后,等待響應(yīng)這段時(shí)間內(nèi)如果遇上網(wǎng)絡(luò)異常導(dǎo)致連接被斷開(kāi),無(wú)法得知服務(wù)端處理情況,如果全部重試可能會(huì)造成服務(wù)端重復(fù)處理;
另外,服務(wù)端和瀏覽器之間的中間代理設(shè)備也不一定支持 HTTP 管道,這給管道技術(shù)的普及引入了更多復(fù)雜性;
基于這些原因,HTTP 管道技術(shù)無(wú)法大規(guī)模使用,我們需要尋找其他方案。實(shí)際上,在 HTTP/1 時(shí)代,連接數(shù)優(yōu)化不外乎兩個(gè)方面:開(kāi)源和節(jié)流。
開(kāi)源
這里說(shuō)的開(kāi)源,當(dāng)然不是「Open Source」那個(gè)開(kāi)源。既然一個(gè) TCP 連接同時(shí)只能處理一個(gè) HTTP 消息,那多開(kāi)幾條 TCP 連接不就解決這個(gè)問(wèn)題了。是的,瀏覽器確實(shí)是這么做的,HTTP/1.1 初始版本中允許瀏覽器針對(duì)同一個(gè)域名同時(shí)創(chuàng)建兩個(gè)連接,在修訂版(rfc7230)中更是去掉了這個(gè)限制。實(shí)際上,現(xiàn)代瀏覽器一般允許同域名并發(fā) 6~8 個(gè)連接。這個(gè)數(shù)字為什么不能更大呢?實(shí)際上這是出于公平性的考慮,每個(gè)連接對(duì)于服務(wù)端來(lái)說(shuō)都會(huì)帶來(lái)一定開(kāi)銷(xiāo),如果瀏覽器不加以限制,一個(gè)性能好帶寬足的終端就可能耗盡服務(wù)端所有資源,造成其他人無(wú)法使用。
但是,現(xiàn)在包含幾十個(gè) CSS、JSS,幾百?gòu)垐D片的頁(yè)面大有所在。為了進(jìn)一步榨干瀏覽器,開(kāi)更多的源,往往我們還會(huì)對(duì)靜態(tài)資源做域名散列,將頁(yè)面靜態(tài)資源分散在多個(gè)子域下加載。多域名能提高并發(fā)連接數(shù),也會(huì)帶來(lái)很多問(wèn)題,例如:
如果同一資源在不同頁(yè)面被散列到不同子域下,會(huì)導(dǎo)致無(wú)法利用之前的 HTTP 緩存;
每個(gè)域名的第一個(gè)連接都要經(jīng)歷 DNS 解析的過(guò)程,這在移動(dòng)端可能需要耗費(fèi)幾百毫秒;
更多的并發(fā)連接 + Keep-Alive 機(jī)制,會(huì)顯著增加服務(wù)端和客戶端的負(fù)擔(dān);
這里稍微吐槽下:本地 TCP 連接和本地端口也是一種資源,為了做 WEB 性能優(yōu)化,開(kāi)更多的域名讓瀏覽器創(chuàng)建更多的并發(fā)連接,是很霸道和不公平的做法。
另外,HTTP/1 協(xié)議頭部使用純文本格式,沒(méi)有任何壓縮,且包含很多冗余信息(例如 Cookie、UserAgent 每次都會(huì)攜帶),所以一個(gè)頁(yè)面的請(qǐng)求數(shù)越多,頭部帶來(lái)的額外開(kāi)銷(xiāo)就越大。我們一般會(huì)用短小且獨(dú)立的域名來(lái)托管靜態(tài)資源,就是為了減小這個(gè)開(kāi)銷(xiāo)(域名越短請(qǐng)求頭起始行的 URI 就越短,獨(dú)立域名不會(huì)共享主域的 Cookie,可以有效減小請(qǐng)求頭大小,這個(gè)策略一般稱(chēng)之為 Cookie-Free Domain)。
節(jié)流
由于我們不能無(wú)限制開(kāi)源,所以節(jié)流也很重要。除了砍掉頁(yè)面內(nèi)容,第二次訪問(wèn)時(shí)利用 HTTP 緩存之外,通常能做的就只有合并請(qǐng)求了。根據(jù)合并的內(nèi)容不同,一般又分為以下幾種:
異步接口合并(Batch Ajax Request);
圖片合并,雪碧圖(CSS Sprite);
CSS、JS 合并(Concatenation);
CSS、JS 內(nèi)聯(lián)(Inline);
圖片、音頻內(nèi)聯(lián)(Data URI);
上面這份列表并不完整,我也沒(méi)打算列全,這些就足以說(shuō)明 HTTP/1 時(shí)代我們?cè)谛阅苌纤鲞^(guò)的不懈努力了??上?,他們并不完美,分別列舉一下他們的缺點(diǎn):
異步接口合并:批量接口返回的時(shí)間受木桶效應(yīng)影響,最慢的那個(gè)接口拖累了其他接口。
圖片合并:首先,為了顯示一張小圖,而不得不加載合并后的整張大圖,一是可能浪費(fèi)流量;二是占用更多內(nèi)存。其次,合并圖片中任何一處修改,都會(huì)導(dǎo)致整張大圖緩存失效。這些問(wèn)題可以根據(jù)不同場(chǎng)景,選用 Data URI、Icon Font、SVG 等技術(shù)來(lái)改造。另外,雪碧圖的生成和維護(hù)都比較繁瑣,最好使用工具自動(dòng)管理。
CSS、JS 合并:合并后的資源需要整體加載完才開(kāi)始解析、執(zhí)行。原本加載完一個(gè)文件就可以解析并執(zhí)行一個(gè),將很多個(gè)文件合并成一個(gè)巨無(wú)霸,會(huì)整體推后可用時(shí)間。為此,Chrome 新版引入了 Script Streaming 技術(shù),能邊加載邊解析 JS 文件。Gmail 為了解決這個(gè)問(wèn)題,將多個(gè) JS 文件合并為一個(gè)由多個(gè) inline script 片段組成的 html,用 iframe 引入,以達(dá)到邊加載變解析執(zhí)行的效果。另外,與圖片合并類(lèi)似,CSS、JS 合并也會(huì)遇到「無(wú)論多小的改動(dòng),都會(huì)導(dǎo)致整個(gè)合并文件緩存失效」的問(wèn)題。
CSS、JS 內(nèi)聯(lián):上篇文章我詳細(xì)分析過(guò)內(nèi)聯(lián)的優(yōu)點(diǎn)和弊端。主要兩個(gè)問(wèn)題:1)無(wú)法利用緩存;2)多頁(yè)面無(wú)法共享。
圖片、音頻內(nèi)聯(lián):除了也有上面兩個(gè)問(wèn)題之外,二進(jìn)制文件以 Data URI 方式內(nèi)聯(lián),需要進(jìn)行 Base64 編碼,體積會(huì)變大 1/3。
結(jié)論
HTTP/1 時(shí)代,我們?yōu)榱斯?jié)省昂貴的 HTTP 連接(TCP 連接),采用了各種優(yōu)化手段,這些方案或多或少會(huì)引入一些問(wèn)題,但是相比收益來(lái)說(shuō)還是值得做,也應(yīng)該做。但是,有了 HTTP/2 的多路復(fù)用和頭部壓縮,HTTP 連接變得可以隨心所欲了,本文提到的這些連接數(shù)優(yōu)化手段確實(shí)可以退休了。
哦對(duì)了,據(jù)官方預(yù)測(cè),HTTP/1 至少還需要 10 年才能徹底退出歷史舞臺(tái),另外盡管 HTTP/2 協(xié)議允許脫離 TSL 部署,但 Chrome 和 Firefox 都表示不支持非 TLS 的 HTTP/2,之后很可能一個(gè)網(wǎng)站會(huì)同時(shí)提供 HTTP/1.1、HTTP/1.1 over TLS、HTTP/2 over TLS 三種服務(wù)。如何在每種情況下,都能給用戶提供最好的體驗(yàn),需要更加深入的優(yōu)化研究和更加精細(xì)的優(yōu)化策略。由此可見(jiàn),在很長(zhǎng)一段時(shí)間內(nèi),WEB 性能優(yōu)化非但不會(huì)落幕,反而會(huì)更加重要。