[譯] HTTP/2 Server Push 詳解
原文:https://www.smashingmagazine.com/2017/04/guide-http2-server-push/
作者:Jeremy Wagner
譯者按:網路優化一直是譯者長期研究的方向,HTTP/2 的理論學習也已做了不少,隨著這項標準的推進,越來越多特性被大家開始使用。作為 HTTP/2 最激動人心的特性,Server Push 在性能提升的效果被寄予了很高期望,卻因其對傳統 B/S 架構的開發模式影響較大未能廣泛實踐。如何更好地使用這項能力,讓我們跟著作者深入探索~
========================譯文分割線===========================
在過去的一年時間,HTTP/2 的出現為關注性能的開發者帶來了顯著的變化。HTTP/2 已經不再是我們期待中的特性,而是伴著 Server Push(服務端推送)能力已然到來。
除了解決常見的 HTTP/1 性能問題(比如,首部阻塞和未壓縮的報頭),HTTP/2 還提供了 Server Push 能力!服務端推送允許我們向用戶發送一些還沒有被訪問的資源。這是一種獲得 HTTP/1 優化實踐(例如內聯)所帶來性能提升的優雅方式,同時也避免了原先實踐的一些缺點。
本文中,你將了解什麼是 Server Push,它的工作原理與解決了哪些問題。同時你也將學習如何使用它,判斷它是否正常運作,以及它對性能的影響。讓我們開始吧!
Server Push 為何物
訪問網站始終遵循著請求——響應模式。用戶將請求發送到遠程伺服器,在一些延遲後,伺服器會響應被請求的內容。
對網路伺服器的初始請求通常是一個 HTML 文檔。在這種情況下,伺服器會用所請求的 HTML 資源進行響應。接著瀏覽器開始對 HTML 進行解析,過程中識別其他資源的引用,例如樣式表、腳本和圖片。緊接著,瀏覽器對這些資源分別發起獨立的請求,等待伺服器返回。
典型的伺服器通信(大圖)
這一機制的問題在於,它迫使用戶等待這樣一個過程:直到一個 HTML 文檔下載完畢後,瀏覽器才能發現和獲取頁面的關鍵資源。從而延緩了頁面渲染,拉長了頁面載入時間。
有了 Server Push,就有了解決上述問題的方案。Server Push 能讓伺服器在用戶沒有明確詢問下,搶先地「推送」一些網站資源給客戶端。只要正確地使用,我們可以根據用戶正在訪問的頁面,給用戶發送一些即將被使用的資源。
比如說你有一個網站,所有的頁面都會在一個名為 styles.css 的外部樣式表中,定義各種樣式。當用戶向務器請求 index.html 時,我們可以在發送 index.html 的同時,向用戶推送 styles.css。
使用HTTP/2 Server Push的Web伺服器通信(大圖)
相比等待伺服器發送 index.html,然後等待瀏覽器請求並接收 styles.css,用戶現在只需等待1次伺服器響應,就可在初次請求同時使用 index.html 和 styles.css。
可以想像,這可以降低頁面的渲染時間。它還解決了一些其他問題,特別是在前端開發工作流方面。
Server Push 解決了什麼問題?
Server Push 解決了減少關鍵內容的網路迴路耗時問題,但這並不是唯一的作用。Server Push 更像是 HTTP/1 特定優化反模式的替代方案,例如將 CSS 和 JavaScript 內聯在 HTML,以及使用 data URI 方案將二進位數據嵌入到 CSS 和 HTML 中。
這些技術在 HTTP/1 優化工作流中非常受用,是因為這樣減少了我們所說的頁面「感知渲染時間」,也就是說在頁面整體載入時間可能不會減少的同時,對用戶而言網頁的載入速度卻顯得更快。這確實是說得通的,如果你將 CSS 內嵌到 HTML 的<style>標籤中,瀏覽器就可以無需等待外部資源的獲取,而立即應用 HTML 中的樣式。這種概念同樣適用於內聯腳本,以及使用 data URI 方式內聯二進位數據。
內聯內容的伺服器通信(大圖)
這看起來是個不錯的方案,對吧?在 HTTP/1 的時代確實如此,因為也沒有別的選擇。而這麼做實際上也留下了惡果,即內聯的內容不能有效地被緩存。若樣式、腳本資源以外鏈及模塊形式引用,會更高效地進行緩存。當用戶訪問後續頁面需要這些資源時,可以直接從緩存中獲取,從而省去了額外的資源請求。
優化緩存行為(大圖)
而當我們對內容進行內聯時,它們是沒有獨立的緩存上下文的,僅存在於所內聯文檔的上下文中。舉個在 HTML 中內聯 CSS 的例子,如果 HTML 的緩存策略,是每次訪問都向伺服器拉取最新的內容,那麼內聯的CSS總是無法緩存其內容。即使把 HTML 進行緩存,但在後續訪問的頁面內,內聯相同的 CSS 內容也是需要重複下載的。這還是比較寬鬆的緩存策略,實際情況中 HTML 僅有較短的緩存周期。內聯是我們在 HTTP/1 優化方案中所做的權衡,它確實在用戶第一次訪問時非常有效,而往往第一印象是非常重要的。
這就是 Server Push 能解決的問題。當推送資源時,我們能獲得與內聯相同的性能提升,同時保持資源的外鏈形式,從而有獨立的緩存策略。這裡有個需要注意的問題,我們稍後再深入探討。
我已經談了很多為什麼你該考慮使用 Server Push 的原因,也澄明了它能為用戶和開發者所解決的問題。接下來讓我告訴你如何去使用它。
如何使用 Server Push
使用Server Push,通常會以下面的方式使用 Link 這個HTTP首部。
Link: </css/styles.css>; rel=preload; as=style
注意我說的是通常,上面看到的實際是預載入資源示意(resource hint)的實踐。這是個區別於 Server Push 的獨立優化方案,但大多數(並非全部)HTTP/2的實現都將 preload 放進了 Link 首部。如果伺服器或客戶端選擇不接受推送的資源,客戶端仍可以根據指示提早獲取資源。
首部中 as=style 部分是必選的,它能告知瀏覽器推送資源的類型。在這個例子中,我們使用了 style 來指明推送的資源是一個樣式表,你還可以設置其他的內容類型。值得注意的是如果省略了 as 的值,會導致瀏覽器對推送資源下載兩次,所以千萬別忘了它。
現在知道推送資源的方法了,但具體要怎樣設置 Link 首部呢?我們有兩種方式:
- Web伺服器配置(例如,Apache httpd.conf 或.htaccess);
- 後端語言功能(例如PHP的 header 方法)。
使用伺服器配置設置 Link 首部
下面是一個 Apache 配置(通過httpd.conf或.htaccess)的例子,作用是在請求 HTML 時推送樣式資源。
<FilesMatch ".html$"> Header set Link "</css/styles.css>; rel=preload; as=style"<FilesMatch>
這裡我們使用了 FilesMatch 指令來匹配後綴為「.html」的文件請求。當一個請求匹配這個條件時,我們就往響應頭裡加入 Link 首部,並告知伺服器推送位置在 /css/styles.css的資源。
邊注:Apache 的 HTTP/2 模塊也可以使用 H2PushResource 指令啟用資源推送。該指令的文檔指出,這種方法能夠早於 Link 首部方法啟用推送。根據 Apache 安裝時的不同設置,你也可能無法使用此功能。本文後面會給出 Link 首部方法的性能測試結果。
截至目前,Nginx 並不支持 HTTP/2 Server Push,目前的 changelog 中沒有任何支持情況的記錄。而隨著 Nginx HTTP/2 實現的逐漸成熟,這種情況可能會發生變化。
使用後端代碼設置 Link 首部
另一個設置 Link 首部的方法是使用伺服器端語言。這在你無法修改或覆蓋伺服器配置時十分有效。下面是 PHP header 方法設置 Link 首部的例子:
header("Link: </css/styles.css>; rel=preload; as=style");
如果你的應用程序部署在一個共享的託管環境中,並且修改伺服器的配置不太現實,那麼這個方法可能是最適合你的。你可以使用任何服務端語言設置這個首部。在真實使用前記得確保測試無誤,以避免潛在的運行時錯誤。
多資源推送
目前看到的都是演示推送一個資源的例子,如果想一次推送更多資源呢?這麼做也是很有道理的,對吧?畢竟頁面不止是樣式表組成的。下面來看推送多資源的例子:
Link: </css/styles.css>; rel=preload; as=style, </js/scripts.js>; rel=preload; as=script, </img/logo.png>; rel=preload; as=image
當你想推送多個資源,只要用逗號把每個指令隔開就行了。因為資源示意是通過 Link 首部加入的,這種語法讓我們可以把不同資源的推送指令合在一起。這還有個包括 preconnect 的混合推送指令示例:
Link: </css/styles.css>; rel=preload; as=style, <https://fonts.gstatic.com>; rel=preconnect
多個 Link 首部也是同樣合法的。下面是 Apache 給 HTML 配置多個 Link 首部的例子:
<FilesMatch ".html$"> Header add Link "</css/styles.css>; rel=preload; as=style" Header add Link "</js/scripts.js>; rel=preload; as=script"<FilesMatch>
這種語法相比一長串逗號分隔的字元串更為方便,且達到的作用是相同的。唯一的缺點就是沒那麼緊湊,而且會多一點位元組量的網路傳輸,但提供的便利是值得的。
現在知道了如何推送資源,我們繼續看推送是否生效。
如何分辨 Server Push 是否生效
目前,我們已經通過 Link 首部來告訴伺服器推送一些資源。剩下的問題是,我們怎麼知道是否生效了呢?
這還要看不同瀏覽器的情況。最新版本Chrome將在開發者工具的網路發起欄中展示推送的資源。
Chrome顯示伺服器推送的資源(大圖)
更進一步,如果把滑鼠懸停在網路請求瀑布圖中的資源上,將獲得關於該推送資源的詳細耗時信息:
Chrome顯示推送資源的詳細耗時信息(大圖)
Firefox對推送資源則標識地沒那麼明顯。如果一個資源是被推送的,則瀏覽器開發者工具的網路信息里,會將其狀態顯示為一個灰色圓點。
Firefox對推送資源的展示(大圖)
如果你在尋找一個確保能分辨資源是否為推送的方法,可以使用 nghttp 命令行客戶端來檢查是否來自 HTTP/2 伺服器,像這樣:
nghttp -ans https://jeremywagner.me
這個命令會顯示出會話中所有資源的匯總結果。推送的資源將在輸出中顯示一個星號(*),像這樣:
id responseEnd requestStart process code size request path 13 +50.28ms +1.07ms 49.21ms 200 3K / 2 +50.47ms * +42.10ms 8.37ms 200 2K /css/global.css 4 +50.56ms * +42.15ms 8.41ms 200 157 /css/fonts-loaded.css 6 +50.59ms * +42.16ms 8.43ms 200 279 /js/ga.js 8 +50.62ms * +42.17ms 8.44ms 200 243 /js/load-fonts.js 10 +74.29ms * +42.18ms 32.11ms 200 5K /img/global/jeremy.png 17 +87.17ms +50.65ms 36.51ms 200 668 /js/lazyload.js 15 +87.21ms +50.65ms 36.56ms 200 2K /img/global/book-1x.png 19 +87.23ms +50.65ms 36.58ms 200 138 /js/debounce.js 21 +87.25ms +50.65ms 36.60ms 200 240 /js/nav.js 23 +87.27ms +50.65ms 36.62ms 200 302 /js/attach-nav.js
這裡,我在自己的站點上使用了 nghttp,有五個推送的資源(至少在寫這篇文章時)。推送的資源在 requestStart 欄左側以星號標記了出來。
現在我們知道了如何識別推送的資源,接下里具體看看對真實站點的性能有什麼實際影響。
測量 Server Push 性能
測量任何性能提升的效果都需要很好的測試工具。Sitespeed.io 是一個可從 npm 獲取的優秀工具,它可以自動地測試頁面,收集有價值的性能數據。有了得力的工具,我們來快速過一下測試方法吧。
測試方法
我想通過一個有意義的方法,來測量 Server Push 對網站性能的影響。為了讓結果是有意義的,我需要建立6種獨立的場景來交叉對比。這些場景主要以兩個方面進行分隔:使用 HTTP/2 或 HTTP/1。在 HTTP/2 伺服器上,我們想測量 Server Push 在多個指標的效果。在 HTTP/1 伺服器上,我們想看看內聯資源的方法,在相同指標中對性能有什麼影響,因為內聯應該能達到和 Server Push 差不多的效果。具體場景如下:
- 未使用 Server Push 的HTTP/2
網站使用了 HTTP/2 協議,但沒有資源是被推送的。
- 僅推送 CSS 的 HTTP/2
使用了 Server Push,但僅用在了 CSS 資源。該網站的 CSS 體積比較小,經過 Brotli 壓縮後僅有2KB多一點。
- 推送所有資源
網站的所有資源都是推送的。包括了上面的 CSS,以及6個JS(合計 1.4KB)、5個SVG圖片(合計5.9KB)。這些資源同樣經過了壓縮處理。
- 未內聯資源的HTTP/1
網站只運行在 HTTP/1 上,沒有內聯任何資源,來減少請求數和加快渲染速度。
- 只內聯 CSS
只有網站的 CSS 被內聯了。
- 內聯所有資源
頁面上的所有資源都進行了內聯。CSS 和腳本是普通內聯,而 SVG 圖片是經過 Base64 編碼方式直接放入 HTML 標籤中。值得一提的是 Base64 編碼後體積比原先大了1.37倍。
在每個場景中,都使用下面的命令開始測試:
sitespeed.io -d 1 -m 1 -n 25 -c cable -b chrome -v https://jeremywagner.me
如果想知道這個命令的輸入、輸出,可以參看文檔。簡而言之,這個命令測試了我的網站 Home - Jeremy Wagner 的主頁,使用了下面的條件:
- 頁面中的鏈接無法抓取。只測試指定的頁面。
- 頁面測試25次
- 使用了「有線寬頻」級的網路配置。迴路時間(譯者註:RTT)為28ms,下行帶寬是5000kbps,上行帶寬為1000kbps。
- 測試使用 Google Chrome
每項測試中收集和展示3項指標:
- 首屏渲染時間
頁面在瀏覽器首次展現的時間點。當我們努力讓一個頁面「感覺上」載入很快時,那麼這個指標是我們要盡量降低的。
- DOMContentLoaded 時間
這個是 HTML 完成載入與解析的時間。同步的 JavaScript 代碼會阻塞解析,並導致這個時間增加。在<script>標籤上使用 async 屬性可以避免對解析的阻塞。
- 頁面載入時間
這個是整個頁面完成所有資源載入的耗時。
測試的所有因素都確定後,讓我們看看結果!
測試結果
經過對上述6種場景的測試,我們將結果以圖表形式做了展示。先看看各個場景的首屏渲染時間情況:
首屏渲染時間(大圖)
讓我們先講講圖表是如何設計的。圖中藍色部分代表了首屏渲染的平均時間,橙色部分是90%的情況,灰色部分代表了首屏渲染的最長耗時。
接下來我們討論結果。最慢的情形是未使用任何優化的 HTTP/2 和 HTTP/1。可以看到,對 CSS 使用 Server Push 使頁面渲染平均速度提升了8%,而內聯 CSS 也比簡單的 HTTP/1 提升了5%速度。
當我們儘可能地推送了所有資源,圖片卻顯示出了一些異樣,首屏渲染時間有所輕微增加。在 HTTP/1 中我們儘可能內聯所有資源,性能表現和推送所有資源差不多,僅僅少了一點時間。
結論很明確:使用 Server Push,我們能獲得比 HTTP/1 中使用內聯更優的性能。但隨著推送或內聯的資源增多,提升的效果逐漸減少。
使用 Server Push 或內聯雖好,但對於首次訪問的用戶並沒有太大價值(譯者註:實際上對於首次訪問用戶有很大的性能提升,猜測作者這裡筆誤了)。另外,這些測試實驗是運行在較少資源的站點上,所以未必能反映出你的網站的使用情況。
我們再看看各項測試對 DOMContentLoaded 時間的影響:
DOMContentLoaded 時間(大圖)
數據趨勢跟剛才看到的圖表沒太大差別,除了一個需要注意的區別:在 HTTP/1 中儘可能地內聯資源,相對 DOMContentLoaded 時間非常低。可能的原因是內聯減少了需要下載的資源數,從而保證解析器(parser)可以不被打斷地工作。
最後再看看頁面載入時間的情況:
頁面載入時間(大圖)
各項測量數據依然保持了先前的趨勢。僅推送 CSS 時載入時間最短。推送所有資源會偶爾導致服務遲緩,但畢竟還是比什麼都不做表現更優。與內聯相比,Server Push 的各項情況都是優於內聯的。
在做最後總結前,還要講講使用 Server Push 時可能遇到的問題。
使用 Server Push 的一些建議
Server Push 並不是性能優化的萬金油,它也有一些需要注意的地方。
推送過多資源
前面的一項測試中,我推送了很多資源,但它們加起來也只佔傳輸數據的一小部分。一次推送很多大資源的話,會造成頁面渲染及可交互時間的延遲,因為瀏覽器不但要載入 HTML 文檔,還要同時下載推送的資源。最好的做法是有選擇性地推送,樣式表文件是個不錯的開始(目前它們並不是很大),接著再評估還有什麼其他資源適合推送。
推送頁面以外的資源
如果你有訪客統計分析,那麼這種做法也未必不好。一個好的例子是,在多頁註冊賬戶表單場景,可以推送下一頁的註冊步驟資源。但要澄清的是,如果你不確定用戶是否會訪問後續的頁面,千萬不要嘗試推送它的資源。有些用戶的流量是十分珍貴的,這麼做可能會導致其不必的損失。
正確地配置 HTTP/2 服務
有些伺服器會給出很多 Server Push 的配置選項。Apache 的 mod_http2 模塊有一些關於如何推送資源的配置選項。H2PushPriority 設置就比較有意思,雖然在我的伺服器上使用了默認設置。有一些實驗性的配置可以獲得額外的性能提升。每一種 Web伺服器都有其整套不同的實驗性配置,所以查看你的伺服器手冊,看看有哪些配置可以用起來吧!
推送資源可能不被緩存
Server Push 也有一些有損性能的的情況,對於訪問網站的回頭客們,一些資源可能會被非必要地進行推送。有些伺服器會儘可能地減輕這種影響。Apache 的 mod_http2 模塊使用了 H2PushDiarySize 設置對這一點進行了一些優化。H2O 伺服器有一種 Server Push 緩存感知特性,使用了 Cookie 機制來記錄推送行為。
如果你不是使用 H2O伺服器,也可以使用服務端代碼實現同樣的效果,即只推送 Cookie 記錄外的資源。如果有興趣了解具體做法,可以查看我在 CSS Tricks 上的文章。值得一提的是,瀏覽器可以向伺服器發送一個 RST_STREAM 幀來通知不需推送的資源。隨著時間推移,這個問題的解決將會愈加優雅。
最後來總結一下以上學到的內容。
最後的思考
如果你已經將自己的網站遷移到 HTTP/2,你沒有什麼理由不使用伺服器推送。如果你的網站因有過多的資源而顯得複雜,可以從體積較小的資源開始嘗試。一個好的經驗法則是,考慮推送那些你曾經用到內聯的資源。推送 CSS 是個不錯的開始。如果感覺更有冒險精神之後,就考慮推送其他資源。要牢記在改動後測試對性能的影響。下了一定功夫後,你一定能從中有所受益。
如果你沒有用像 H2O 這樣使用緩存感知推送機制的伺服器,可以考慮用 cookie 追蹤你的用戶,只在沒有相關 cookie 的情況下給他們推送資源。這樣可以為未知用戶提升著性能的同時,最小化向已知用戶的資源推送量。這不僅利於性能優化,也向用戶展示了數據用量的尊重。
剩下的就需要你自己在伺服器上折騰 Server Push 了,看看有哪些特性可以對你或用戶有用吧。如果你想了解更多關於 Server Push,看看這些資源吧:
- 「Server Push,」 「Hypertext Transfer Protocol Version 2 (HTTP/2),」 Internet Engineering Task Force
- 「Modernizing Our Progressive Enhancement Delivery,」 Scott Jehl, Filament Group
- 「Innovating with HTTP 2.0 Server Push,」 Ilya Grigorik
推薦閱讀: