在連續寫了兩篇關于「HTTP/2 與 WEB 性能優化」的文章后,今天來寫這個系列的最后一篇。在正式開始之前,我們先來簡單回顧下之前兩篇文章:
「[HTTP/2 與 WEB 性能優化(一)](http://imququ.com/post/http2-and-wpo-1.html)」的結論是:HTTP/2 的 Server Push 機制,可以讓重要的 JS、CSS 等資源盡快加載,從而不再需要 HTTP/1 中「將重要資源內聯在頁面頭部」的優化方案了。
「[HTTP/2 與 WEB 性能優化(二)](http://imququ.com/post/http2-and-wpo-2.html)」的結論是:HTTP/2 支持了多路復用,HTTP 連接變得十分廉價,之前為了節省連接數所采用的類似于「資源合并、資源內聯」等優化手段不再需要了。多路復用可以在一個 TCP 連接上建立大量 HTTP 連接,也就不存在 HTTP 連接數限制了,HTTP/1 中常見的「靜態域名」優化策略不但用不上了,還會帶來負面影響,需要去掉。另外,HTTP/2 的頭部壓縮功能也能大幅減少 HTTP 協議頭部帶來的開銷。
但 HTTP/2 并不是萬能的,并不是說用了 HTTP/2 就不再需要性能優化了。我在本系統第二篇文章末尾寫到:
> 據官方預測,HTTP/1 至少還需要 10 年才能徹底退出歷史舞臺,另外盡管 HTTP/2 協議允許脫離 TSL 部署,但 Chrome 和 Firefox 都表示不支持非 TLS 的 HTTP/2,之后很可能一個網站會同時提供 HTTP/1.1、HTTP/1.1 over TLS、HTTP/2 over TLS 三種服務。如何在每種情況下,都能給用戶提供最好的體驗,需要更加深入的優化研究和更加精細的優化策略。
實際上,除了前兩篇文章中提到的這些需要為 HTTP/2 做出調整的優化策略之外,其余大部分 HTTP/1 時期的優化策略依然有效。HTTP/1 的 WPO 并不是什么新鮮話題,大家早就熟門熟路了,本文只打算列舉其中幾個:
## 啟用壓縮
壓縮的目的是讓傳輸的數據變得更小。我們的線上代碼(JS、CSS 和 HTML)都會做壓縮,圖片也會做壓縮(PNGOUT、Pngcrush、JpegOptim、Gifsicle 等)。對于文本文件,在服務端發送響應之前進行 GZip 壓縮也很重要,通常壓縮后的文本大小會減小到原來的 1/4 - 1/3。對代碼進行內容壓縮已經有成熟的工具和標準流程了,而服務端的 GZip 更是標配,所以「壓縮」是一項收益投入比很高的優化手段。
## 使用 HTTP 緩存
任何一個 WEB 項目,要提高性能,各個環節的緩存必不可少。利用好 HTTP 協議的緩存機制,可以大幅減少傳輸數據,減少請求,這又是一項收益投入比超高的優化手段。這里把之前我寫的 HTTP/1.1 緩存機制介紹翻出來:
首先,服務端可以通過響應頭里的?`Last-Modified`(最后修改時間) 或者?`ETag`(內容特征) 標記實體。瀏覽器會存下這些標記,并在下次請求時帶上?`If-Modified-Since: 上次 Last-Modified 的內容`?或?`If-None-Match: 上次 ETag 的內容`,詢問服務端資源是否過期。如果服務端發現并沒有過期,直接返回一個狀態碼為 304、正文為空的響應,告知瀏覽器使用本地緩存;如果資源有更新,服務端返回狀態碼 200、新的 Last-Modified、Etag 和正文。這個過程被稱之為 HTTP 的協商緩存,通常也叫做弱緩存。
可以看到協商緩存并不會節省連接數,但是在緩存生效時,會大幅減小傳輸內容(304 響應沒有正文,一般只有幾百字節)。另外為什么有兩個響應頭都可以用來實現協商緩存呢?這是因為一開始用的?`Last-Modified`?有兩個問題:1)只能精確到秒,1 秒內的多次變化反映不出來;2)時間采用絕對值,如果服務端 / 客戶端時間不對都可能導致緩存失效。HTTP/1.1 并沒有規定?`ETag`?的生成規則,而一般實現者都是對資源內容做摘要,能解決前面兩個問題。
另外一種緩存機制是服務端通過響應頭告訴瀏覽器,在什么時間之前(Expires)或在多長時間之內(Cache-Control: Max-age=xxx),不要再請求服務器了。這個機制我們通常稱之為 HTTP 的強緩存。
一旦資源命中強緩存規則后,再次訪問完全沒有 HTTP 請求(Chrome 開發者工具的 Network 面板依然會顯示請求,但是會注明 from cache;Firefox 的 firebug 也類似,會注明 BFCache),這會大幅提升性能。所以我們一般會對 CSS、JS、圖片等資源使用強緩存,而入口文件(HTML)一般使用協商緩存或不緩存,這樣可以通過修改入口文件中對強緩存資源的引入 URL 來達到即時更新的目的。
這里也解釋下為什么有了?`Expire`,還要有?`Cache-Control`。也有兩個原因:1)Cache-Control 功能更強大,對緩存的控制能力更強;2)Cache-Control 采用的 max-age 是相對時間,不受服務端 / 客戶端時間不對的影響。
另外關于瀏覽器的刷新(F5 / cmd + r)和強刷(Ctrl + F5 / shift + cmd +r):普通刷新會使用協商緩存,忽略強緩存;強刷會忽略瀏覽器所有緩存(并且請求頭會攜帶 Cache-Control:no-cache 和 Pragma:no-cache,用來通知所有中間節點忽略緩存)。只有從地址欄或收藏夾輸入網址、點擊鏈接等情況下,瀏覽器才會使用強緩存。
## 減少 DNS 查詢
我們知道,建立 TCP 連接需要知道目標 IP,而絕大部分時候給瀏覽器的是域名。瀏覽器需要先將域名解析為 IP,這個過程就是 DNS 查詢,一般需要幾毫秒到幾百毫秒,移動環境下會更慢。DNS 解析完成之前,請求會被 Block。瀏覽器一般都會緩存 DNS 查詢結果,頁面使用的域名(包括子域名)越少,花費在 DNS 查詢上的開銷就越小。另外,合理使用瀏覽器的 DNS Prefetching 技術,也是很好的做法。
## 減少重定向
無論是通過服務端響應頭產生的重定向,還是通過?`<meta>`?或者 JS 產生的重定向,都可能引入新的 DNS 查詢、新的 TCP 連接以及新的 HTTP 請求,所以減少重定向也很重要。瀏覽器基本都會緩存通過?`301 Moved Permanently`?指定的跳轉,所以對于永久性跳轉,可以考慮使用狀態碼?`301`。對于啟用了 HTTPS 的網站,配置?[HSTS](http://imququ.com/post/web-security-and-response-header.html#toc-0)?策略,也可以減少從 HTTP 到 HTTPS 的重定向。
WEB 性能優化是一個系統工程,不可能在這一篇文章里寫完,我決定先就寫到這兒。最后,推薦一個 Chrome 擴展:[HTTP/2 and SPDY indicator](https://chrome.google.com/webstore/detail/http2-and-spdy-indicator/mpbpobfflnpcgagjijhmgnchggcjblin),它可以在地址欄顯示當前網站是否啟用了 SPDY 或者 HTTP/2,點擊圖標可以直接打開 Chrome 的 HTTP/2 的調試界面,十分方便。