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