自從我這樣擼代碼之後,公司網頁的瀏覽量提升了107%!

歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~html

本文由 騰訊IVWEB團隊 發表於 雲+社區專欄

做者:yangchunwen前端

HTTP協議是前端性能乃至安全中一個很是重要的話題,最近在看《web性能權威指南(High Performance Browser Networking)》,把其中關於HTTP部分的內容拿出來分享一下,加了一點本身的想法,固然沒有《HTTP權威指南》講得詳細,但對於理解咱們日常作的事情頗有啓發。預計會有兩三篇文章,重點分別會涉及到HTTP 1.一、HTTPS、HTTP 2.0等內容,本篇主要涉及HTTP 1.1及其應用。web

HTTP的歷史

HTTP 0.9

HTTP的第一個版本被官方稱爲HTTP0.9,這是個只有一行的協議,例如:瀏覽器

GET /about/

(超文本響應……)
(鏈接關閉……)

HTTP 0.9有幾個要點:緩存

  • 客戶端/服務器、請求/響應協議
  • ASCII 協議,運行於TCP/IP連接之上
  • 設計用來傳輸超文本文檔(HTML)
  • 服務器與客戶端之間的鏈接在每次請求以後都會關閉

這個版本的HTTP主要用來傳輸文本,而且沒有共用TCP鏈接。安全

HTTP 1.0

一個典型的HTTP 1.0請求過程以下:性能優化

GET /rfc/rfc1945.txt HTTP/1.0 
User-Agent: CERN-LineMode/2.15 libwww/2.17b3 
Accept: */* 

HTTP/1.0 200 OK 
Content-Type: text/plain 
Content-Length: 137582
Expires: Thu, 01 Dec 1997 16:00:00 GMT 
Last-Modified: Wed, 1 May 1996 12:45:26 GMT Server: Apache 0.84

(超文本響應……)
(鏈接關閉……)

相對前一個版本,HTTP 1.0主要有如下幾點變化:服務器

  • 請求和相應能夠因爲多行首部字段構成
  • 響應對象前面添加了一個響應狀態
  • 響應對象不侷限於超文本
  • 服務器與客戶端之間的鏈接在每次請求以後都會關閉
  • 實現了Expires等傳輸內容的緩存控制
  • 內容編碼Accept-Encoding、字符集Accept-Charset等協商內容的支持

這時候開始有了請求及返回首部的概念,開始傳輸不限於文本(其餘二進制內容)cookie

HTTP 1.1

HTTP 1.1是當前大部分應用所使用的協議版本。相對前面的1.0版本,HTTP 1.1語義格式基本保持不變,可是它加入了不少重要的性能優化:持久鏈接、分塊編碼傳輸、字節範圍請求、加強的緩存機制、傳輸編碼及請求管道網絡

實際上,持久連接在後來被反向移植到了HTTP1.0上

HTTP 2.0

HTTP 2.0 的主要目標是改進傳輸性能,實現低延遲和高吞吐量。HTTP 2.0做了不少性能角度的優化,另外一方面,HTTP的高層協議語義並不會由於此次版本升級而受影響。全部HTTP首部、值,以及它們的使用場景都不會變。現有的任何網站和應用,無需作任何修改均可以在 HTTP 2.0 上跑起來。換句話說, 等之後咱們的服務器、客戶端(如瀏覽器)都支持HTTP 2.0的時候,咱們不用爲了利用 HTTP 2.0 的好處而修改標記,做不少額外的編碼,卻能享受到它帶來的更低的延遲和更高的網絡鏈接利用率交付!

HTTP 2.0的內容將在下篇或下下篇放出,本文不對其作過多潤色

HTTP 1.1與前端性能

前面講到,HTTP 1.1這個版本引入了大量加強性能的重要特性,其中包括:

  • 持久化鏈接以支持鏈接重用
  • 分塊傳輸編碼以支持流式響應
  • 請求管道以支持並行請求處理
  • 字節服務以支持基於範圍的資源請求
  • 改進的更好的緩存機制

這裏重點講一下持久化、管道在前端性能優化中的一些應用

持久鏈接

所謂持久鏈接,就是重用 TCP鏈接,多個HTTP請求公用一個TCP鏈接。

HTTP 1.1 改變了 HTTP 協議的語義,默認使用持久鏈接。換句話說,除非明確告知(經過 Connection: close 首部),不然服務器默認會保持TCP鏈接打開。若是你使用的是 HTTP 1.1,從技術上說不須要 Connection: Keep-Alive 首部,但不少客戶端仍是選擇加上它,好比咱們的瀏覽器在發起請求的時候,通常會默認幫咱們帶上 Connection: Keep-Alive 首部。

咱們來看一下爲何持久鏈接對咱們來講這麼重要。

假設一個網頁僅包含一個HTML文檔及一個CSS樣式文件,服務器響應這兩個文件的時間分別爲40ms及20ms,服務器和瀏覽者分別在哈爾濱和深圳,二者之間單向光纖延遲爲28ms(假設的理想狀態,實際會比這個要大)。

  1. 首先是獲取HTML文檔的請求過程:

img

HTML下載完畢後,TCP鏈接關閉。

  1. 其次,發起CSS資源的請求,再次經歷一次TCP握手

img

能夠看到,兩個HTTP請求都分別須要經歷一次TCP的三次握手時間,另外,圖中沒有體現到的是,每一次TCP請求都有可能會經歷一次TCP慢啓動 過程,這是影響傳播性能的一個不可忽視的重要因素。

假如咱們底層的TCP鏈接獲得重用,這時候的狀況會是這樣子:

img

很明顯,在獲取CSS的請求中,減小了一次握手往返。

一開始,每一個請求要用兩個TCP鏈接,總延遲爲284ms。在使用持久鏈接後,避免了一次握手往返,總延遲減小爲228ms。這裏面兩次請求節省了56ms(一個 RTT,Round-Trip Time)的時間

上面的例子還只是只有一個HTML和一個CSS的簡單假設狀況,而現實世界的web的HTTP請求數量比這個要多得多,在啓用持久鏈接的狀況下,N次請求節省的總延遲時間就是(N-1)×RTT。

現實狀況中,延遲更高、請求更多,性能提高效果比這裏還要高得多。事實上,網絡延遲越高,請求越多,節省的時間就越多。實際應用中,這個節省的總時間可按秒來算了。若是每個HTTP都重啓一個TCP鏈接,可想而知要浪費多少時間!

HTTP管道

持久 HTTP 可讓咱們重用已有的鏈接來完成屢次應用請求,但屢次請求必須嚴格知足先進先出(FIFO,first in first out)的隊列順序:發送請求,等待響應完成,再發送客戶端隊列中的下一個請求。

舉一下上一節持久鏈接的那個例子,首先,服務器處理完第一次請求後,會發生了一次完整的往返:先是響應回傳,接着是第二次請求,在第二次請求到達服務器之間的這段時間裏,服務器空閒。

若是服務器能在處理完第一次請求後,當即開始處理第二次請求呢?甚至,若是服務器能夠並行處理兩個請求呢?

這時候HTTP管道就派上用場了,HTTP管道是一個很小但對上述工做流很是重要的一次優化。

有了HTTP管道,咱們的HTTP請求在必定程度上不用再一個一個地串行請求,而是能夠多個並行了,看起來好像很理想:

img

如上圖,HTML和CSS的請求同時到達服務器,服務器同時處理,而後返回。

這一次,經過使用HTTP管道,又減小了兩次請求之間的一次往返,總延遲減小爲 172 ms。從一開始沒有持久鏈接、沒有管道的284ms,到優化後的172ms,這40%的性能提高徹底拜簡單的協議優化所賜。

等一下,剛剛那個例子好像哪裏還不夠好:既然請求同時到達,同時處理,爲何後面要先返回HTML,而後再返回CSS?二者不能同時返回嗎?

理想很豐滿,現實卻有點骨感,這就是HTTP 1.1管道的一個很大的侷限性:HTTP請求沒法很好地利用多路複用,不容許一個鏈接上的多個響應數據交錯返回(多路複用)。於是一個響應必須徹底返回後,下一個響應纔會開始傳輸。

這個管道只是讓咱們把FIFO隊列從客戶端遷移到了服務器。也就是說,請求能夠同時到達服務器,服務器也能夠同時處理兩個文件,可是,兩個文件仍是得按順序返回給用戶,以下圖:

img

  • HTML和CSS請求同時到達,但先處理的是HTML請求
  • 服務器並行處理兩個請求,其中處理 HTML 用時40ms,處理CSS用時20ms
  • CSS請求先處理完成,但被緩衝起來以等候HTML響應先發送
  • 發送完HTML響應後,再發送服務器緩衝中的CSS響應

能夠看到,即便客戶端同時發送了兩個請求,並且CSS資源先準備就緒,可是服務器也會先發送 HTML 響應,而後再交付 CSS。

題外話 上面兩節舉的例子,說到了HTML和CSS請求同時到達,這是書中的例子,實際上,我的以爲這個例子舉得不是很恰當。 實際的web中,HTML及其包含的CSS通常不會同時到達服務器,正常的 瀑布圖也不是這樣的,每每是要先獲取HTML內容後瀏覽器才能發起其中的CSS等資源請求。我想做者只是爲了闡述原理吧,我的認爲換成同一個HTML文檔中CSS和JS可能更加恰當。

這個問題的原理在於TCP層面的「隊首阻塞」,感興趣能夠去複習下計算機網絡的課程。其代價每每是:不能充分利用網絡鏈接,形成服務器緩衝開銷,有可能致使客戶端更大的延遲。更嚴重的時,假如前面的請求無限期掛起,或者要花很長時間才能處理完,全部後續的請求都將被阻塞,等待它完成。

因此,在HTTP 1.1中,管道技術的應用很是有限,儘管其優勢毋庸置疑。實際上,一些支持管道的瀏覽器,一般都將其做爲一個高級配置選項,但大多數瀏覽器都會禁用它。換句話說,做爲前端工程師,開發的應用是面向普通瀏覽器應用的話,仍是不要過多的期望HTTP管道,看來仍是期待一下HTTP 2.0中對管道的優化吧。

不過,實際上仍是有很好地利用HTTP管道的一些應用,例如在WWDC 2012上,有蘋果的工程師分享了一個針對HTTP優化取得巨大成效的案例:經過使用HTTP的持久鏈接和管道,重用iTunes中既有的TCP鏈接,使得低網速用戶的性能提高到原來的3倍!

實際上假如你想充分利用管道的好處,必需要保證下面這幾點條件:

  • HTTP客戶端支持管道
  • HTTP服務器支持管道
  • 應用能夠處理中斷的鏈接並恢復
  • 應用能夠處理中斷請求的冪等問題
  • 應用能夠保護自身不受出問題的代理的影響

由於iTunes的服務器和客戶端都受開發者控制的應用,因此他們能知足以上的條件。這也許能給開發hybrid應用或者開發瀏覽器以外的web應用的前端工程師們一些啓發,若是你開發的網站面向的用戶是使用五花八門的瀏覽器,你可能就沒轍了。

使用多個TCP鏈接

由於HTTP 1.1管道存在上面的缺點,因此利用率不高。那麼問題來了:假設沒有使用HTTP管道,咱們的全部HTTP請求都只能經過持久鏈接,一個接一個地串行返回,這得有多慢?

實際上,現階段的瀏覽器廠商採起了另外的辦法來解決HTTP 1.1管道的缺陷:容許咱們並行打開多個TCP會話。至因而多少個,你們可能已經似曾相識:4到8個不等。這就是前端工程師很是熟悉的瀏覽器只容許從同一個服務器並行加載4到8個資源這一認識的真正來歷。

HTTP持久鏈接雖然幫咱們解決了TCP鏈接複用的問題,可是現階段的HTTP管道卻沒法實現多個請求結果的交錯返回,因此瀏覽器只能開啓多個TCP鏈接,以達到並行地加載資源的目的。

只能說,這是做爲繞過應用協議(HTTP)限制的一個權宜之計。能夠這樣打一個比喻,一個水管沒法同時運輸多種液體,那就只能給每一種液體開通一條運輸管了,至於這個水管何時能夠智能化到同時運輸不一樣的液體,又能保證各自完整不受干擾到達目的地並在目的地自行分類?仍是那一句,期待HTTP 2.0吧。

這裏的鏈接數爲何是4到8個,是多方平衡的結果:這個數字越大,客戶端和服務器的資源佔用越多(在高併發訪問的服務器中由於TCP鏈接形成的系統開銷不可忽視),每一個主機4到8個鏈接只不過是你們都以爲比較安全的一個數字。

域名分區

前面說到,瀏覽器和服務器之間只能併發4到8個TCP鏈接,也就是同時下載4到8個資源,夠嗎?

看看咱們如今的大部分網站,動不動就幾十個JS、CSS,一次六個,會形成後面大量的資源排隊等待;另外,只下載6個資源,對帶寬的利用率也是很低的。

打個比喻,一個工廠裝了100根水管,每次卻只能用其中6根接水,既慢,又浪費水管!

因此,咱們前端性能優化中有一個最佳實踐:使用域名分區

對啊,何須把本身只限制在一個主機上呢?咱們能夠手工將全部資源分散到多個子域名,因爲主機名稱不同了,就能夠突破瀏覽器的鏈接限制,實現更高的並行能力。

經過這種方式「欺騙」瀏覽器,這樣瀏覽器和服務器之間的並行傳輸數量就變多了。

域名分區使用得越多,並行能力就越強!

可是,域名分區也是有代價的!

實踐中,域名分區常常會被濫用。

例如,假設你的應用面向的是2G網絡的手機用戶,你分配了好幾個域名,同時加載十幾二十多個CSS、JS,這裏的問題在於:

  • 每個域名都會多出來的DNS查詢開銷,這是額外的機器資源開銷和額外的網絡延時代價。2G網絡的DNS查詢可不像你公司的電腦同樣,相反多是好幾秒的延遲
  • 同時加載多個資源,以2G網絡那種小得可憐的帶寬來看,後果每每就是帶寬被佔滿,每個資源都下載得很慢
  • 手機的耗電加快

因此在一些低帶寬高延時的場景,例如2G手機網絡,域名分區作過了的話,不光不會帶來前端性能的提高,反而會變成性能殺手。

域名分區是一種合理但又不完美的優化手段,最合適的辦法就是,從最小分區數目(不分區)開始,而後逐個增長分區並度量分區後對應用的影響,從而獲得一個最優的域名數。

鏈接與拼合

咱們前端性能優化中有這麼一個所謂的最佳實踐原則:合併打包JS、CSS文件,以及作CSS sprite。

如今咱們應該知道爲何要這樣作了,實際上就是由於如今HTTP 1.1的管道太弱了,這兩種技術的效果就好像是隱式地啓用了HTTP 管道:來自多個響應的數據先後相繼地鏈接在一塊兒,消除了額外的網絡延遲。

實際上,就是把管道提升了一層,置入了應用中,也許到了HTTP 2.0時代,前端工程師就不用幹這樣的活了吧?(HTTP 2.0的內容下篇講)

固然,鏈接拼合技術一樣有代價的。

  • 例如CSS sprite,瀏覽器必須分析整個圖片,即使實際上只顯示了其中的一小塊,也要始終把整個圖片都保存在內存中。瀏覽器沒有辦法把不顯示的部分從內存中剔除掉。
  • 再者,既然JS、CSS合併了,帶來的通常就是體積的增大,在帶寬有限的環境下(例如2G)下載時間就變長,通常致使的就是頁面渲染時間延後等後果。由於JavaScript 和CSS 處理器都不容許遞增式執行的,對於JavaScript 和CSS 的解析及執行,則要等到整個文件下載完畢。
打包文件到底多大合適呢?惋惜的是,沒有理想的大小。然而,谷歌PageSpeed團隊的測試代表,30~50 KB(壓縮後)是每一個JavaScript 文件大小的合適範圍:既大到了可以減小小文件帶來的網絡延遲,還能確保遞增及分層式的執行。具體的結果可能會因爲應用類型和腳本數量而有所不一樣。

資源內嵌

JavaScript 和CSS 代碼, 經過適當的script 和style 塊能夠直接放在頁面中,而圖片甚至音頻或PDF 文件,均可以經過數據URI(data:mediatype,data)的方式嵌入到頁面中。

上面的這種方式咱們稱爲資源內嵌

嵌入資源是另外一種很是流行的優化方法, 把資源嵌入文檔能夠減小請求的次數。尤爲在2G網絡等狀況中,內嵌資源能夠有效地減小屢次請求帶來的時延。能夠參考這篇文章在2G中的一些實踐。

固然,有缺點:

  • 內嵌方式的資源,不能被瀏覽器、CDN 或其餘緩存代理做爲單獨的資源緩存。若是在多個頁面中都嵌入一樣的資源,那麼這個資源將會隨着每一個頁面的加載而被加載,從而增大每一個頁面的整體大小。
  • 若是嵌入資源更新,那麼全部之前出現過它的頁面都將被宣告無效,而由客戶端從新從服 務器獲取。
  • 圖片等非文本性資源經過base64 編碼,會致使開銷明顯增大:編碼後的資源大小比原大小增大33%!

Google的磚家給出一些經驗:

  • 只考慮嵌入1~2 KB 如下的資源,由於小於這個標準的資源常常會致使比它自身更高的HTTP 開銷
  • 若是文件很小,並且只有個別頁面使用,能夠考慮嵌入。理想狀況下,最好是隻用一次的資源
  • 若是文件很小,但須要在多個頁面中重用,應該考慮集中打包
  • 若是小文件常常須要更新,就不要嵌入了
  • 經過減小 HTTP cookie 的大小將協議開銷最小化

小結

本文介紹了HTTP 1.1在前端性能優化中的一些應用,有些是爲了繞過HTTP 1.1侷限性的一些不得不作的事情,好比資源合併、壓縮、內嵌等,這些均可以說是HTTP 2.0來臨前的一些解決問題的「黑魔法」。

HTTP 1.1及其利用固然遠遠沒有本文說得那麼簡單,我只是濃縮了一部份內容,有興趣能夠去研究《HTTP權威指南》。

問答
BDD框架的前端如何搭建?
相關閱讀
全面進階 H5 直播(上)
NodeJs內存管理
WebGL 紋理顏色原理
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識

此文已由做者受權騰訊雲+社區發佈,更多原文請點擊

搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社區

相關文章
相關標籤/搜索