最近粗線了很多 HTTP2 相關的帖子和討論,感受新一輪的潮流在造成,因此最近找了本 HTTP2 相關書籍作知識儲備,恰好記成筆記以備後詢 ~css
這本書自己不錯,缺點就是翻譯的有點蹩腳,另外由於是 2017 年出的書,因此有些內容時效性不太好,好比關於 Chrome 的部分,因此我根據 Chrome 的官方文檔增長了點內容 😅html
若是但願獲取本書的 PDF 資源,能夠關注文末二維碼加微信羣找羣主要~前端
OPTIONS
方法、Upgrade
首部、Range
請求、壓縮和傳輸編碼、管道化等功能。由於強制要求客戶端提供 Host 首部,因此虛擬主機託管成爲可能,也就是在一個 IP 上提供多個 Web 服務。另外使用了 keep-alive
以後,Web 服務器也不須要在每一個響應以後關閉鏈接。這對於提高性能和效率而言意義重大,由於瀏覽器不再用爲每一個請求從新發起 TCP 鏈接了。HTTP2 被但願達到如下特性:web
不少網站已經在用HTTP/2(h2)了,好比 Facebook、Instagram、Twitter 等,下面介紹如下如何本身搭建 h2 服務器。要運行 h2 服務器,主要分兩步:ajax
證書能夠經過三種方式獲取:算法
前兩個方法 將建立自簽名證書,僅用於測試,因爲不是 CA 簽發的,瀏覽器會報警chrome
後面關於建立 h2 服務器的步驟就不記了,能夠百度下api
從用戶在瀏覽器中點擊連接到頁面呈如今屏幕上,在此期間到底發生了什麼?瀏覽器請求 Web 頁面時,會執行重複流程,獲取在屏幕上繪製頁面須要的全部信息。爲了更容易理解,咱們把這一過程分紅兩部分:資源獲取、頁面解析/渲染。promise
資源請求流程圖:瀏覽器
流程爲:
資源響應流程圖:
頁面上的每一次點擊,都須要重複執行前面那些流程,給網絡帶寬和設備資源帶來壓力。Web 性能優化的的核心,就是加快甚至乾脆去掉其中的某些步驟。
下面網絡級別的性能指標,它會影響整個 Web 頁面加載。
HTTP/1 的問題天然是 HTTP/2 要解決的核心問題
瀏覽器不多隻從一個域名獲取一份資源,通常但願能同時獲取許多資源。h1 有個特性叫管道化(pipelining),容許一次發送一組請求,可是隻能按照發送順序依次接收響應。管道化備受互操做性和部署的各類問題的困擾,基本沒有實用價值。
在請求應答過程當中,若是出現任何情況,剩下全部的工做都會被阻塞在那次請求應答以後。這就是『隊頭阻塞』,它會阻礙網絡傳輸和 Web 頁面渲染,直至失去響應。爲了防止這種問題,現代瀏覽器會針對單個域名開啓 6 個鏈接,經過各個鏈接分別發送請求。它實現了某種程度上的並行,可是每一個鏈接仍會受到的影響。
傳輸控制協議(TCP)的設計思路是:對假設狀況很保守,並可以公平對待同一網絡的不一樣流量的應用。涉及的核心概念就是擁塞窗口(congestion window)。『擁塞窗口』是指,在接收方確認數據包以前,發送方能夠發出的 TCP 包的數量。 例如,若是擁塞窗口指定爲 1,那麼發送方發出 1 個數據包以後,只有接收方確認了那個包,才能發送下一個。
TCP 有個概念叫慢啓動(Slow Start),它用來探索當前鏈接對應擁塞窗口的合適大小。慢啓動的設計目標是爲了讓新鏈接搞清楚當前網絡情況,避免給已經擁堵的網絡繼續添亂。它容許發送者在收到每一個確認回覆後額外發送 1 個未確認包。這意味着新鏈接在收到 1 個確認回覆以後,能夠發送 2 個數據包;在收到 2 個確認回覆以後,能夠發 4 個,以此類推。這種幾何級數增加很快到達協議規定的發包數上限,這時候鏈接將進入擁塞避免階段。
這種機制須要幾回往返數據請求才能得知最佳擁塞窗口大小。但在解決性能問題時,就這區區幾回數據往返也是很是寶貴的時間成本。若是你把一個數據包設置爲最大值下限 1460 字節,那麼只能先發送 5840 字節(假定擁塞窗口爲 4),而後就須要等待接收確認回覆。理想狀況下,這須要大約 9 次往返請求來傳輸完整個頁面。除此以外,瀏覽器通常會針對同一個域名開啓 6 個併發鏈接,每一個鏈接都免不了擁塞窗口調節。
傳統 TCP 實現利用擁塞控制算法會根據數據包的丟失來反饋調整。若是數據包確認丟失了,算法就會縮小擁塞窗口。這就相似於咱們在黑暗的房間摸索,若是腿碰到了桌子就會立刻換個方向。若是遇到超時,也就是等待的回覆沒有按時抵達,它甚至會完全重置擁塞窗口並從新進入慢啓動階段。新的算法會把其餘因素也考慮進來,例如延遲,以提供更妥善的反饋機制。
前面提到過,由於 h1 並不支持多路複用,因此瀏覽器通常會針對指定域名開啓 6 個併發鏈接。這意味着擁塞窗口波動也會並行發生 6 次。TCP 協議保證那些鏈接都能正常工做,可是不能保證它們的性能是最優的。
雖然 h1 提供了壓縮被請求內容的機制,可是消息首部卻沒法壓縮。消息首部可不能忽略,儘管它比響應資源小不少,但它可能佔據請求的絕大部分(有時候多是所有)。若是算上 cookie,就更大了。
消息首部壓縮的缺失也容易致使客戶端到達帶寬上限,對於低帶寬或高擁堵的鏈路尤爲如此。『體育館效應』(Stadium Effect)就是一個經典例子。若是成千上萬人同一時間出如今同一地點(例如重大致育賽事),會迅速耗盡無線蜂窩網絡帶寬。這時候,若是能壓縮請求首部,把請求變得更小,就可以緩解帶寬壓力,下降系統的總負載。
若是瀏覽器針對指定域名開啓了多個 socket(每一個都會受隊頭阻塞問題的困擾),開始請求資源,這時候瀏覽器能指定優先級的方式是有限的:要麼發起請求,要麼不發起。然而 Web 頁面上某些資源會比另外一些更重要,這必然會加劇資源的排隊效應。這是由於瀏覽器爲了先請求優先級高的資源,會推遲請求其餘資源。可是優先級高的資源獲取以後,在處理的過程當中,瀏覽器並不會發起新的資源請求,因此服務器沒法利用這段時間發送優先級低的資源,總的頁面下載時間所以延長了。還會出現這樣的狀況:一個高優先級資源被瀏覽器發現,可是受制於瀏覽器處理的方式,它被排在了一個正在獲取的低優先級資源以後。
現在 Web 頁面上請求的不少資源徹底獨立於站點服務器的控制,咱們稱這些爲第三方資源。現代 Web 頁面加載時長中每每有一半消耗在第三方資源上。雖然有不少技巧能把第三方資源對頁面性能的影響降到最低,可是不少第三方資源都不在 Web 開發者的控制範圍內,因此極可能其中有些資源的性能不好,會延遲甚至阻塞頁面渲染。使人掃興的是, h2 對此也一籌莫展。
2010 年,谷歌把 Web 性能做爲影響頁面搜索評分的重要因素之一,性能指標開始在搜索引擎中發揮做用。對於不少 Web 頁面,瀏覽器的大塊時間並非用於呈現來自網站的主體內容(一般是 HTML),而是在請求全部資源並渲染頁面。
所以,Web 開發者逐漸更多地關注經過減小客戶端網絡延遲和優化頁面渲染性能來提高Web 性能。
在與服務主機創建鏈接以前,須要先解析域名;那麼,解析越快就越好。下面有一些方法:
<link rel="dns-prefetch" href="//ajax.googleapis.com">
本章前面提到過,開啓新鏈接是一個耗時的過程。若是鏈接使用 TLS(也確實應該這麼作),開銷會更高。下降這種開銷的方法以下
preconnect
指令,鏈接在使用以前就已經創建好了,這樣處理流程的關鍵路徑上就沒必要考慮鏈接時間了,preconnect
不光會解析 DNS,還會創建 TCP 握手鍊接和 TLS 協議(若是須要)<link rel="preconnect" href="//fonts.example.com" crossorigin>
若是要從同一個域名請求大量資源,瀏覽器將自動開啓到服務器的併發鏈接,避免資源獲取瓶頸。雖然如今大部分瀏覽器支持 6 個或更多的併發鏈接數目,但你不能直接控制瀏覽器針對同一域名的併發鏈接數。
重定向一般觸發與額外域名創建鏈接。在移動網絡中,一次額外的重定向可能把延遲增長數百毫秒,這不利於用戶體驗,並最終會影響到網站上的業務。簡單的解決方案就是完全消滅重定向,由於對於重定向的使每每並無合理緣由。若是它們不能被直接消滅,你還有兩個選擇:
沒有什麼比直接從本地緩存獲取資源來得更快,由於它根本就不須要創建網絡鏈接。
能夠經過 HTTP 首部指定 cache control 以及鍵 max-age
(以秒爲單位),或者 expires
首部。
我的信息(用戶偏好、財務數據等)絕對不能在網絡邊緣緩存,由於它們不能共享。時間敏感的資源也不該該緩存,例如實時交易系統上的股票報價。這就是說,除此以外其餘一切都是能夠緩存的,即便僅僅緩存幾秒或幾分鐘。對於那些不是常常更新,然而一旦有變化就必須馬上更新的資源,例如重大新聞,能夠利用各大 CDN 廠商提供的緩存清理(purging)機制處理。這種模式被稱爲『一直保留,直到被通知』(Hold til Told),意思是永久緩存這些資源,等收到通知後才刪除。
若是緩存 TTL 過時,客戶端會向服務器發起請求。在多數狀況下,收到的響應其實和緩存的版本是同樣的,從新下載已經在緩存裏的內容也是一種浪費。HTTP 提供條件請求機制,客戶端能以有效方式詢問服務器:『若是內容變了,請返回內容自己;不然,直接告訴我內容沒變。』當資源不常常變化時,使用條件請求能夠顯著節省帶寬和性能;可是,保證資源的最新版迅速可用也是很是重要的。使用條件緩存能夠經過如下方法。
Last-Modified-Since
。僅當最新內容在首部中指定的日期以後被更新過,服務器才返回完整內容;不然只返回 304 響應碼,並在響應首部中附帶上新的時間戳 Date
字段。ETag
;它惟一標識所請求的資源。ETag 由服務器 提供,內嵌於資源的響應首部中。服務器會比較當前 ETag
與請求首部中收到的 ETag
,若是一致,就只返回 304 響應碼;不然返回完整內容。通常來講,大多數 Web 服務器會對圖片和 CSS/JS 使用這些技術,但你也能夠將其用到其餘資源。
全部的文本內容(HTML、JS、CSS、SVG、XML、JSON、字體等),均可以壓縮和極簡化。這兩種方法組合起來,能夠顯著減小資源大小。更少字節數對應着更少的請求與應答,也就意味着更短的請求時間。
極簡化(minification, 混淆)是指從文本資源中剝離全部非核心內容的過程。一般,要考慮方便人類閱讀和維護,而瀏覽器並不關心可讀性,放棄代碼可讀性反而能節省空間。在極簡化的基礎上,壓縮能夠進一步減小字節數。它經過可無損還原的算法減小資源大小。在發送資源以前,若是服務器進行壓縮處理,能夠節省 90% 的大小。
在屏幕上繪製第一個像素以前,瀏覽器必須確保 CSS 已經下載完整。儘管瀏覽器的預處理器很智能,會盡早請求整個頁面所須要的 CSS,可是把 CSS 資源請求放在頁面靠前仍然是種最佳實踐,具體位置是在文檔的 head 標籤裏,並且要在任何 JS 或圖片被請求和處理以前。
默認狀況下,若是在 HTML 中定位了 JS,它就會被請求、解析,而後執行。在瀏覽器處理完這個 JS 以前,會阻止其後任何資源的下載渲染。然而大多數時候,這種默認的阻塞行爲致使了沒必要要的延遲,甚至會形成單點故障。爲了減輕 JS 阻塞帶來的潛在影響,下面針對己方資源(你能控制的)和第三方資源(你不能控制的)推薦了不一樣的策略
若是 JS 執行順序可有可無,而且必須在 onload
事件觸發以前運行,那麼能夠設置 async
屬性,像這樣:
<script async src="/js/myfile.js">
只需作到下載 JS 與解析 HTML 並行,就能極大地提高總體用戶體驗。慎用 document.write
指令,由於極可能中斷頁面執行,因此須要仔細測試。
若是 JS 執行順序很重要,而且你也能承受腳本在 DOM 加載完以後運行,那麼請使用 defer 屬性。像這樣
<script defer src="/js/myjs.js">
onload
事件,能夠考慮經過 iframe
獲取 JS,由於它的處理獨立於主頁面。可是,經過 iframe
下載的 JS 訪問不了主頁面上的元素。對大多數網站而言,圖片的重要性和比重在不斷增長。既然圖片主導了多數現代網站,優化它們就可以得到最大的性能回報。全部圖片優化手段的目標都是在達到指定視覺質量的前提下傳輸最少的字節。
HTTP/2 對每一個域名只會開啓一個鏈接,因此 HTTP/1.1 下的一些訣竅對它來講只會拔苗助長。
詳細看 6.7 節
HTTP/1.1 孕育了各類性能優化手段與訣竅,能夠幫助咱們深刻理解 Web 及其內部實現。HTTP/2 的目標之一就是淘汰掉衆多(並非所有)此類訣竅。
在升級到 HTTP/2 以前,你應該考慮:
任何不支持 h2 的客戶端都將簡單地退回到 h1,並仍然能夠訪問你的站點基礎設施。
全部主流瀏覽器只能訪問基於 TLS(即 HTTPS 請求)的 h2。
Web 開發者以前花費了大量心血來充分使用 h1,而且已經總結了一些訣竅,例如資源合併、域名拆分、極簡化、禁用 cookie 的域名、生成精靈圖,等等。因此,當得知這些實踐中有些在 h2 下變成反模式時,你可能會感到吃驚。例如,資源合併(把不少 CSS/JS 文件拼合成一個)能避免瀏覽器發出多個請求。對 h1 而言這很重要,由於發起請求的代價很高;可是在 h2 的世界裏,這部分已經作了深度優化。放棄資源合併的結果多是,針對單個資源發起請求的代價很低,但瀏覽器端能夠進行更細粒度的緩存。
詳細看 6.7 節
本章將全面探討 HTTP/2 的底層工做原理,深刻到數據層傳輸的幀及其通訊方式。
HTTP/2 大體能夠分爲兩部分
h2 有些特色須要關注一下:
與徹底無狀態的 h1 不一樣的是,h2 把它所承載的幀(frame)和流(stream)共同依賴的鏈接層元素捆綁在一塊兒,其中既包含鏈接層設置也包含首部表。也就是說,與以前的 HTTP 版本不一樣,每一個 h2 鏈接都有必定的開銷。之因此這麼設計,是考慮到收益遠遠超過其開銷。
在鏈接的時候,HTTP/2 提供兩種協議發現的機制:
在協議制定過程當中,很早就把小數點去掉了,這代表將來的 HTTP 版本不能保證語義的向後兼容,也就是說只有 HTTP/2 沒有 HTTP/2.0、HTTP/2.2
HTTP/2 是基於幀(frame)的協議,採用分幀是爲了將重要信息都封裝起來,讓協議的解析方能夠輕鬆閱讀、解析並還原信息。 相比之下,h1 不是基於幀的,而是以文本分隔。因此解析 h1 的請求或響應可能會出現如下問題:
從另外一方面來講,有了幀,處理協議的程序就能預先知道會收到什麼。下圖是一個 HTTP/2 幀的結構
前 9 個字節對於每一個幀是一致的。解析時只須要讀取這些字節,就能夠準確地知道在整個幀中指望的字節數。其中每一個字段的說明以下
名稱 | 長度 | 描述 |
---|---|---|
Length | 3 字節 | 表示幀負載的長度(取值範圍爲 2^14~2^24-1 字節)。 請注意,214 字節是默認的最大幀大小,若是須要更大的幀,必須在 SETTINGS 幀中設置 |
Type | 1 字節 | 當前幀類型 |
Flags | 1 字節 | 具體幀類型的標識 |
R | 1 位 | 保留位,不要設置,不然可能帶來嚴重後果 |
Stream Identifier | 31 位 | 每一個流的惟一 ID |
Frame Payload | 長度可變 | 真實的幀內容,長度是在 Length 字段中設置的 |
相比依靠分隔符的 h1,h2 還有另外一大優點:若是使用 h1 的話,你須要發送完上一個請求或者響應,才能發送下一個;因爲 h2 是分幀的,請求和響應能夠交錯甚至多路複用。多路複用有助於解決相似隊頭阻塞的問題。
h2 有十種不一樣的幀類型:
名稱 | ID (Type) | 描述 |
---|---|---|
DATA | 0x0 | 數據幀,傳輸流的核心內容 |
HEADERS | 0x1 | 報頭幀,包含 HTTP 首部,和可選的優先級參數 |
PRIORITY | 0x2 | 優先級幀,指示或者更改流的優先級和依賴 |
RST_STREAM | 0x3 | 流終止幀,容許一端中止流(一般因爲錯誤致使的) |
SETTINGS | 0x4 | 設置幀,協商鏈接級參數 |
PUSH_PROMISE | 0x5 | 推送幀,提示客戶端,服務器要推送些東西 |
PING | 0x6 | PING 幀,測試鏈接可用性和往返時延(RTT) |
GOAWAY | 0x7 | GOAWAY 幀,告訴另外一端,當前端已結束 |
WINDOW_UPDATE | 0x8 | 窗口更新幀,協商一端將要接收多少字節(用於流量控制) |
CONTINUATION | 0x9 | 延續幀,用以擴展 HEADER 數據塊 |
擴展幀 :HTTP/2 內置了名爲擴展幀的處理新的幀類型的能力。依靠這種機制,客戶端和服務器的實現者能夠實驗新的幀類型,而無需制定新協議。按照規範,任何客戶端不能識別的幀都會被丟棄,因此網絡上新出現的幀就不會影響核心協議。固然,若是你的應用程序依賴於新的幀,而中間代理會丟棄它,那麼可能會出現問題。
HTTP/2 規範中的流(stream):HTTP/2 鏈接上獨立的、雙向的幀序列交換。你能夠將流看做在鏈接上的一系列幀,用來傳輸一對請求/響應消息。若是客戶端想要發出請求,它會開啓一個新的流,服務器也在這個流上回復。這與 h1 的請求、響應流程相似,區別在於,由於有分幀,因此多個請求和響應能夠交錯,而不會互相阻塞。流 ID(幀首部的第 6~9 字節)用來標識幀所屬的流。
客戶端到服務器的 h2 鏈接創建以後,經過發送 HEADERS
幀來啓動新的流,若是首部須要跨多個幀,可能還發會送 CONTINUATION
幀。
HTTP 消息泛指 HTTP 請求或響應。一個消息至少由 HEADERS 幀(用來初始化流)組成,而且能夠另外包含 CONTINUATION
和 DATA
幀,以及其餘的 HEADERS
幀。 下圖是普通 GET 請求的示例流程
POST 和 GET 的主要差異之一就是 POST 請求一般包含客戶端發出的大量數據。下圖是 POST 消息對應的各幀可能的樣子
h1 的請求和響應都分紅消息首部和消息體兩部分;與之相似,h2 的請求和響應分紅 HEADERS
幀和 DATA
幀。HTTP/1 和 HTTP/2 消息的下列差異是須要注意
h2 的新特性之一是基於流的流量控制。h1 中只要客戶端能夠處理,服務端就會盡量快地發送數據,h2 提供了客戶端調整傳輸速度的能力,服務端也一樣能夠調整傳輸的速度。
WINDOW_UPDATE
幀用於執行流量控制功能,能夠做用在單獨某個流上(指定具體 Stream Identifier
)也能夠做用整個鏈接 (Stream Identifier
爲 0x0),只有 DATA
幀受流量控制影響。初始化流量窗口後,發送多少負載,流量窗口就減小多少,若是流量窗口不足就沒法發送,WINDOW_UPDATE
幀能夠增長流量窗口大小。流創建的時候,窗口大小默認 65535(2^16-1)字節。
流的最後一個重要特性是依賴關係。
現代瀏覽器會盡可能以最優的順序獲取資源,由此來優化頁面性能。在沒有多路複用的時候,在它能夠發出對新對象的請求以前,須要等待前一個響應完成。有了 h2 多路複用的能力,客戶端就能夠一次發出全部資源的請求,服務端也能夠當即着手處理這些請求。由此帶來的問題是,瀏覽器失去了在 h1 時代默認的資源請求優先級策略。假設服務器同時接收到了 100 個請求,也沒有標識哪一個更重要,那麼它將幾乎同時發送每一個資源,次要元素就會影響到關鍵元素的傳輸。
h2 經過流的依賴關係來解決上面這個問題。經過 HEADERS
幀和 PRIORITY
幀,客戶端能夠明確地和服務端溝通它須要什麼,以及它須要這些資源的順序。這是經過聲明依賴關係樹和樹裏的相對權重實現的。
流能夠被標記爲依賴其餘流,所依賴的流完成後再處理當前流。每一個依賴 (dependency) 後都跟着一個權重 (weight),這一數字是用來肯定依賴於相同的流的可分配可用資源的相對比例。
其餘時候也能夠經過 PRIORITY
幀調整流優先級。
設置優先級的目的是爲了讓端點表達它所指望對端在併發的多個流之間如何分配資源的行爲。更重要的是,當發送容量有限時,能夠使用優先級來選擇用於發送幀的流。
升單個對象性能的最佳方式,就是在它被用到以前就放到瀏覽器的緩存裏面。這正是 h2 服務端推送的目的。
若是服務器決定要推送一個對象(RFC 中稱爲『推送響應』),會構造一個 PUSH_PROMISE
幀。這個幀有不少重要屬性:
PUSH_PROMISE
幀首部中的流 ID (Promised Stream ID)用來響應相關聯的請求。推送的響應必定會對應到客戶端已發送的某個請求。若是瀏覽器請求一個主體 HTML 頁面,若是要推送此頁面使用的某個 JavaScript 對象,服務器將使用請求對應的流 ID 構造 PUSH_PROMISE
幀。PUSH_PROMISE
幀的首部塊與客戶端請求推送對象時發送的首部塊是類似的。因此客戶端有辦法放心檢查將要發送的請求。:method
首部的值必須確保安全。安全的方法就是冪等的那些方法,這是一種不改變任何狀態的好辦法。例如,GET 請求被認爲是冪等的,由於它一般只是獲取對象,而 POST 請求被認爲是非冪等的,由於它可能會改變服務器端的狀態。PUSH_PROMISE
幀應該更早發送,應當早於客戶端接收到可能承載着推送對象的 DATA
幀。假設服務器要在發送 PUSH_PROMISE
以前發送完整的 HTML,那客戶端可能在接收到 PUSH_PROMISE
以前已經發出了對這個資源的請求。h2 足夠健壯,能夠優雅地解決這類問題,但仍是會有些浪費。PUSH_PROMISE
幀會指示將要發送的響應所使用的流 ID若是客戶端對 PUSH_PROMISE
的任何元素不滿意,就能夠按照拒收緣由選擇重置這個流(使用 RST_STREAM
),或者發送 PROTOCOL_ERROR
(在 GOAWAY
幀中)。常見的狀況是緩存中已經有了這個對象。
假設客戶端不拒收推送,服務端會繼續進行推送流程,用 PUSH_PROMISE
中指明 ID 對應的流來發送對象
若是服務器接收到一個頁面的請求,它須要決定是推送頁面上的資源仍是等客戶端來請求。決策的過程須要考慮到以下方面
若是服務器選擇正確,那就真的有助於提高頁面的總體性能,反之則會損耗頁面性能。
現代網頁平均有不少請求,這些請求之間幾乎沒有新的的內容,這是極大的浪費。
首部列表 (Header List) 是零個或多個首部字段 (Header Field) 的集合。當經過鏈接傳送時,首部列表經過壓縮算法(即下文 HPACK) 序列化成首部塊 (Header Block),不用 GZIP 是由於它有泄漏加密信息的風險。HPACK 是種表查找壓縮方案,它利用霍夫曼編碼得到接近 GZIP 的壓縮率。
而後,序列化的首部塊又被劃分紅一個或多個叫作首部塊片斷 (Header Block Fragment) 的字節序列,並經過 HEADERS
、PUSH_PROMISE
,或者 CONTINUATION
幀進行有效負載傳送。
假設客戶端按順序發送以下請求首部:
Header1: foo Header2: bar Header3: bat
當客戶端發送請求時,能夠在首部數據塊中指示特定首部及其應該被索引的值。它會建立一張表:
索引 | 首部名稱 | 值 |
---|---|---|
62 | Header1 | foo |
63 | Header2 | bar |
64 | Header3 | bat |
若是服務端讀到了這些請求首部,它會照樣建立一張表。客戶端發送下一個請求的時候, 若是首部相同,它能夠直接發送:62 63 64
,服務器會查找先前的表格,並把這些數字還原成索引對應的完整首部。首部壓縮機制中每一個鏈接都維護了本身的狀態。
HPACK 的實現比這個要複雜得多,好比:
:method: GET
在靜態表中索引爲 2。按規定,靜態表包含 61 個條目,因此上例索引編號從 62 開始。關於字段如何索引,有不少控制規則:
:path: /foo.html
,其值每次都不一樣)線上傳輸的 h2 信息是通過壓縮的二進制數據。
下面是一個簡單的 h2 的 get 請求
:authority: www.akamai.com :method: GET :path: / :scheme: https accept: text/html,application/xhtml+xml,... accept-language: en-US,en;q=0.8 cookie: sidebar_collapsed=0; _mkto_trk=... upgrade-insecure-requests: 1 user-agent: Mozilla/5.0 (Macintosh;...
下面是 h2 的一個響應
:status: 200 cache-control: max-age=600 content-encoding: gzip content-type: text/html;charset=UTF-8 date: Tue, 31 May 2016 23:38:47 GMT etag: "08c024491eb772547850bf157abb6c430-gzip" expires: Tue, 31 May 2016 23:48:47 GMT link: <https://c.go-mpulse.net>;rel=preconnect set-cookie: ak_bmsc=8DEA673F92AC... vary: Accept-Encoding, User-Agent x-akamai-transformed: 9c 237807 0 pmb=mRUM,1 x-frame-options: SAMEORIGIN
在這個響應中,服務器表示請求已成功受理(狀態碼 200),設置了 cookie(cookie 首部),表示返回的內容使用 gzip 壓縮(content-encoding 首部)
HTTP/2 大部分狀況下傳輸 web 頁面比 HTTP/1.1 快。
網絡條件相同,使用不一樣瀏覽器客戶端,一樣的網站頁面加載性能可能差異很大。
延遲是指數據包從一個端點到另外一個端點所花的時間。有時,它也表示數據包到達接收方而後返回發送方所需的時間,又稱爲往返時延(RTT),長度通常以毫秒計。
影響延遲的因素衆多,但有兩個是最重要的:端點間的距離,以及所用傳輸介質
兩點之間的網線不會是筆直的,另外各類網關、路由器、交換機以及移動基站等(也包括服務器應用自己)都會增長延遲
若是網絡中傳輸的數據包沒有成功到達目的地,就會發生丟包,這一般是由網絡擁堵形成的。
頻繁丟包會影響 h2 的頁面傳輸,主要是由於 h2 開啓單一 TCP 鏈接,每次有丟包/擁堵時,TCP 協議就會縮減 TCP 窗口。
若是 TCP 流上丟了一個數據包,那麼整個 h2 鏈接都會停頓下來,直到該數據包重發並被接收到。
服務端推送讓服務器具有了在客戶端請求以前就推送資源的能力。 測試代表,若是合理使用推送,頁面渲染時間能夠減小 20%~50%。
然而,推送也會浪費帶寬,這是由於服務端可能試圖推送那些在客戶端已經緩存的資源,致使客戶端收到並不須要的數據。客戶端確實能夠發送 RST_STREAM 幀來拒絕服務器的 PUSH_PROMISE
幀,可是 RST_STREAM
並不會即刻到達,因此服務器仍是會發送一些多餘的信息。
若是用戶第一次訪問頁面時,就能向客戶端推送頁面渲染所需的關鍵 CSS 和 JS 資源,那麼服務端推送的真正價值就實現了。不過,這要求服務器端實現足夠智能,以免『推送承諾』(push promise)與主體 HTML 頁面傳輸競爭帶寬。
理想狀況下,服務端正在處理 HTML 頁面主體請求時纔會發起推送。有時候,服務端須要作一些後臺工做來生成 HTML 頁面。這時候服務端在忙,客戶端卻在等待,這正是開始向客戶端推送所需資源的絕佳時機。
首字節時間(TTFB)用於測量服務器的響應能力。是從客戶端發起 HTTP 請求到客戶端瀏覽器收到資源的第一個字節所經歷的時間。由 socket 鏈接時間、發送 HTTP 請求所需時間、收到頁面第一個字節所需時間組成。
h1 中,客戶端針對單個域名在每一個鏈接上依次請求資源,並且服務器會按序發送這些資源。客戶端只有接收了以前請求的資源,纔會再請求剩下的資源,服務器接着繼續響應新的資源請求。這個過程會一直重複,直到客戶端接收完渲染頁面所需的所有資源。
與 h1 不一樣,經過 h2 的多路複用,客戶端一旦加載了 HTML,就會向服務器並行發送大量請求。相比 h1,這些請求得到響應的時間之和通常會更短;可是由於是請求是同時發出的,而單個請求的計時起點更早,因此 h2 統計到的 TTFB 值會更高。
HTTP/2 比 h1 確實作了更多的工做,其目的就是爲了從整體上提高性能。下面是一些 h1 沒有,但 h2 實現了的
下圖是使用 h1 和 h2 加載同一個頁面的加載時序對比,整體來講 h2 體驗更好
許多網站會使用各類統計、跟蹤、社交以及廣告平臺,就會引入各類第三方的資源。
h1 下的一些性能調優辦法在 h2 下會起到副作用。下面列出了一些用於優化 h1 請求的經常使用技巧,並標註了 h2 方面的考慮。
名稱 | 描述 | 備註 |
---|---|---|
資源合併 | 把多個文件(JavaScript、CSS) 合成一個文件,以減小 HTTP 請求 | 在 HTTP/2 下這並不是必要,由於請求的傳輸字節數和時間成本更低,雖然這種成本仍然存在 |
極簡化 | 去除 HTML、JavaScript、CSS 這類文件中無用的代碼 | 很棒的作法,在 HTTP/2 下也要保留 |
域名拆分 | 把資源分佈到不一樣的域名上面去,讓瀏覽器利用更多的 socket 鏈接 | HTTP/2 的設計意圖是充分利用單個 socket 鏈接,而拆分域名會違背這種意圖。建議取消域名拆分,但請注意本表格以後的附註框會介紹這個問題相關的各類複雜狀況 |
禁用 cookie 的域名 | 爲圖片之類的資源創建單獨的域名,這些域名不用 cookie,以儘量減小請求尺寸 | 應該避免爲這些資源單獨設立域名(參見域名拆分),但更重要的是,因爲 HTTP/2 提供了首部壓縮,cookie 的開銷會顯著下降 |
生成精靈圖 | 把多張圖片拼合爲一個文件,使用 CSS 控制在 Web 頁面上展現的部分 | 與極簡化相似,只不過用 CSS 實現這種效果的代價高昂;不推薦在 HTTP/2 中使用 |
精靈圖(spriting)是指把不少小圖片拼合成一張大圖,這樣只需發起一個請求就能夠覆蓋多個圖片元素。在 HTTP/2 中,針對特定資源的請求再也不是阻塞式的,不少請求能夠並行處理;就性能而言,生成精靈圖已失去意義,由於多路複用和首部壓縮去掉了大量的請求開銷。
與之相似,小的文本資源,例如 JS 和 CSS,會依照慣例合併成一份更大的資源,或者直接內嵌在主體 HTML 中,這也是爲了減小客戶端-服務器鏈接數。這種作法有個問題是,那些小的 CSS 或 JS 自身也許可緩存,但若是它們內嵌在不可緩存的 HTML 中的話,固然也就不可緩存了。把不少小的 JS 腳本合併成一個大文件可能仍舊對 h2 有意義,由於這樣能夠更好地壓縮處理並節省 CPU。
域名拆分(sharding)是爲了利用瀏覽器針對每一個域名開啓多個鏈接的能力來並行下載資源。對於包含大量小型資源的網站,廣泛的作法是拆分域名,以利用現代瀏覽器針能對每一個域名開啓 6 個鏈接的特性,充分利用可用帶寬。
由於 HTTP/2 採起多路複用,因此域名拆分就不是必要的了,而且反而會讓協議力圖實現的目標落空。比較好的辦法就是繼續保持當前的域名拆分,可是確保這些域名共享同一張證書 [ 通配符 / 存儲區域網絡(SAN)],並保持服務器 IP 地址和端口相同,以便從瀏覽器網絡歸併(network coalescence)中收益,這樣能夠節省爲單個域名鏈接創建的時間。
在 HTTP/1 下,請求和響應首部從不會被壓縮。隨着時間推移,首部大小已經增加了,超過單個 TCP 數據包的 cookie
能夠說司空見慣。所以,在內容源和客戶端之間來回傳輸首部信息的開銷可能形成明顯的延遲。
所以,對圖片之類不依賴於 cookie
的資源,設置禁用 cookie
的域名是個合理的建議。
可是 HTTP/2 中,首部是被壓縮的,而且客戶端和服務器都會保留『首部歷史』,避免重複傳輸已知信息。因此,若是你要重構站點,大可沒必要考慮禁用 cookie
的域名,這樣能減小不少包袱。
靜態資源也應該從同一域名提供;使用與主頁面 HTTP 相同的域名,消除了額外的 DNS 查詢以及(潛在的)socket 鏈接,它們都會減慢靜態資源的獲取。把阻塞渲染的資源放在一樣的域名下,也能夠提高性能。
資源預取也是一項 Web 性能優化手段,它提示瀏覽器只要有可能就繼續下載可緩存資源,並把這些資源緩存起來。儘管如此,若是瀏覽器很忙,或者資源下載花的時間太 長,預取請求將會被忽略。資源預取能夠在 HTML 中插入 link 標籤實現:
<link rel="prefetch" href="/important.css">
也能夠使用 HTTP 響應中的 Link
首部: Link: </important.css>; rel=prefetch
資源預取與 h2 引入的服務端推送並沒多少關聯。服務端推送用於讓資源更快到達瀏覽器, 而資源預取相比推送的優勢之一是,若是資源已經在緩存裏,瀏覽器就不會浪費時間和帶寬重複請求它。因此,能夠把它看做 h2 推送的補充工具,而不是將被替代的特性。
網絡丟包是 h2 的命門,一次丟包機會就會讓它的全部優化泡湯。
全部瀏覽器在進行 HTTP/2 傳輸時都須要使用 TLS(HTTPS),即便事實上HTTP/2 規範自己並無強制要求 TLS。這個緣由是:
Upgrade
首部,經過 80 端口(明文的 HTTP 端口)通訊時,通訊鏈路上代理服務器的中斷等因素會致使很是高的錯誤率。若是基於 443 端口(HTTPS 端口)上的 TLS 發起請求,錯誤率會顯著下降,而且協議通訊也更簡潔。HTTP/2 畢竟是新鮮事物,如今不少瀏覽器都支持啓用或禁用 h2。
服務端推送是 h2 中最使人興奮也最難正確使用的特性之一,如今全部的主流瀏覽器都已經支持了此特性。
若是須要創建一個新鏈接,而瀏覽器支持鏈接歸併,那麼經過複用以前已經存在的鏈接,就可以提高請求性能。這意味着能夠跳過 TCP 和 TLS 的握手過程,改善首次請求新域名的性能。若是瀏覽器支持鏈接歸併,它會在開啓新鏈接以前先檢查是否已經創建了到相同目的地的鏈接。
相同目的地具體指的是:已經存在鏈接,其證書對新域名有效,此域名能夠被解析成那個鏈接對應的 IP 地址。若是上述條件都知足,那麼瀏覽器會在已創建的鏈接上向該域名發起 HTTP/2 請求。
若是要經過 h2 傳輸內容,咱們有幾個選擇。支持 HTTP/2 的網絡設施大體有如下兩類。
Web服務器 :一般所說的提供靜態和動態內容服務的程序。
代理/緩存 :通常處在服務器和最終用戶之間,能夠提供緩存以減輕服務器負載,或進行額外加工。許多代理也能扮演 Web 服務器的角色。
在選擇 HTTP/2 服務器時,咱們須要檢查、評估一些關鍵點。除了基本的通用性能、操做系統支持、學習曲線、可擴展性以及穩定性,還應當關注 Web 請求的依賴項和優先級,以及對服務端推送的支持。
內容分發網絡(CDN)是反向代理服務器的全球性分佈式網絡,它部署在多個數據中心。CDN 的目標是經過縮短與最終用戶的距離來減小請求往返次數,以此爲最終用戶提供高可用、高性能的內容服務。
大多數主流 CDN 是支持 HTTP/2 的,選擇 CDN 時主要考慮的兩點是:對服務端推送的支持,以及它們處理優先級的方式。這兩點對現實世界中的 Web 性能影響重大。
Chrome 開發者工具中的 Network 欄,有助於簡單直觀地跟蹤客戶端和服務端的通信,它 按下面表格的形式展現了若干信息:
打開 devtools 的 Network 欄,鼠標放在瀑布流 Waterfall 的資源上,就會看到資源加載過程當中各個階段的詳細時間
Connection Setup (鏈接設置)
Queueing :請求被渲染引擎或者網絡層延遲的時間,瀏覽器在如下狀況下對請求排隊
Connection Start (開始鏈接階段)
Request/Response (請求 / 響應)
其餘
HTTP/2 的弱點之一就是依賴主流 TCP 實現。在 3.1.3 節中已經討論過,TCP 鏈接受制於 TCP 慢啓動、擁塞規避,以及不合理的丟包處理機制。用單個連接承載頁面涉及的全部資源請求,就能享受多路複用帶來的好處;然而面對 TCP 層級的隊首阻塞時,咱們仍是一籌莫展。因此 Google 開發的 QUIC 採納了 HTTP/2 的優勢,而且避免了這些缺點。
網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~
推介閱讀:
PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~
另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~