前端性能精進(六)——網絡

  網絡也是前端性能優化的重要一環,網頁上的資源都要經過網絡來傳輸。

  優化網絡性能除了緩存和壓縮之外,還有就是協議和 CDN。

  HTTP 協議已經歷了多個版本,每個版本的出現其實就是爲了解決已知的性能問題。

  目前市面上,有許多成熟的商業 CDN 服務,採用這些服務的網頁,在性能提升上也很可觀。

一、緩存

  Web 緩存可以自動將資源副本保存到本地,減少了客戶端與服務器之間的通信次數,加速頁面加載,降低網絡延遲,如下圖所示。

  

  緩存的處理過程可以簡單的分爲幾步:

  1. 首先在緩存中搜索指定資源的副本,如果命中就執行第二步;
  2. 第二步就是對資源副本進行新鮮度檢測(也就是文檔是否過期),如果不新鮮就執行第三步;
  3. 第三步是與服務器進行再驗證,驗證通過(即沒有過期)就更新資源副本的新鮮度,再返回這個資源副本(此時的響應狀態碼爲“304 Not Modified”),不通過就從服務器返回資源,再將最新資源的副本放入緩存中。

1)強緩存

  通用首部 Cache-Control 和實體首部 Expires 會爲每個資源附加一個過期日期,相當於食品的保質期。

  在這個保質期內的資源,都會被認爲是新鮮的,也就不會和服務器進行通信,如下圖所示。

  

  這類在瀏覽器中直接判斷緩存是否有效的方式常被稱爲強緩存。

  Expires首部會指定一個具體的過期日期(如下所示),由於很多服務器的時鐘並不同步,所以會有誤差,不推薦使用。

Expires:         Fri, 24 Sep 2027 07:00:32 GMT

  Cache-Control 首部能指定資源處於新鮮狀態的秒數(如下所示),秒數從服務器將資源傳來之時算起,用秒數比用具體日期要靈活很多。

Cache-Control:     max-age=315360

  當緩存的資源副本被同時指定了過期秒數和過期日期(Expires)的時候,會優先處理過期秒數。

  在Cache-Control首部中,有兩個比較混淆的值:no-cache 和 no-store。

  no-cache 字面上比較像禁止資源被緩存,但其實不是,no-store 纔是這個功能。

  no-cache 可以將資源緩存,只是要先與服務器進行新鮮度再驗證,驗證通過後纔會將其提供給客戶端,如下圖所示。

  

  在通用首部中,還有個歷史遺留首部:Pragma。

  Pragma 首部用於實現特定的指令,它也有一個值爲 no-cache,功能和 Cache-Control 中的相同,如下所示。

Cache-Control:     no-cache
Pragma:         no-cache

2)協商緩存

  協商緩存需要與服務器通信後,才能判斷緩存是否過期,常用的驗證方法有兩種,第一種是日期比對法。

  服務器在響應請求的時候,會在響應報文中附加實體首部 Last-Modified,指明資源的最後修改日期,客戶端在緩存資源的同時,也會一併把這個日期緩存。

  當對緩存中的資源副本進行再驗證時,在請求報文中會附加 If-Modified-Since 首部,攜帶最後修改日期,與服務器上的修改日期進行比對,如下圖所示。

  

  第二種是實體標記法,日期比對法非常依賴日期,如果服務器上的日期不準確,再驗證就會出現偏差,這個時候就比較適合用實體標記法。

  服務器會爲每個資源生成唯一的字符串形式的標記(例如 52fdbf98-2663),該標記會保存在實體首部 ETag 中。

  在響應報文中附加 ETag,把標記返回給客戶端,客戶端接收並將其緩存。

  當對緩存中的資源副本進行再驗證時,在請求報文中會附加 If-None-Match 首部,如下圖所示。

  

  只有當攜帶的標記與服務器上的資源標記一致時,才能說明緩存沒有過期,這樣就能返回緩存中的資源。

二、壓縮

  在請求首部中,Accept-Encoding 用於描述客戶端可接受的編碼格式,服務器按指定的編碼格式壓縮數據。

  在實體首部中,Content-Encoding 用戶描述內容編碼格式,告知客戶端用這個編碼格式解壓。

  常用的壓縮算法有 GZip 和 Brotli。對於非媒體文件(例如 HTML、CSS、JavaScript 等)經過壓縮後,尺寸可減少 50%,甚至 80%。

  注意,由於圖像已經被壓縮,再用 GZip 和 Brotli 進行壓縮反而會使尺寸變大。

1)GZip 壓縮

  GZip 是一種基於 Deflate 算法的無損壓縮,Deflate 是同時使用了 LZ77 算法與哈夫曼編碼(Huffman Coding)的一個組合體。

  GZip 對於要壓縮的文件,首先使用 LZ77 算法,然後對得到的結果再使用哈夫曼編碼的方法進行壓縮。

  LZ77 算法會把數據中一些可以組織成短語(最長字符)的字符加入字典,然後再有相同字符出現採用標記來代替字典中的短語。

  例如 ABCCDEFABCCDEGH 通過 LZ77 算法可壓縮爲ABCCDEF(7,6)GH,其中 7 是重複串起始字符 A 到前面串起始字符的距離,6 是重複串的長度(ABCCDE)。

  哈夫曼編碼使用變長編碼表對字符進行編碼,其中變長編碼表是通過一種評估字符出現機率的方法得到的,出現機率高的字符使用較短的編碼,反之則使用較長的編碼。

  例如 ABAABACD 字符串,經過哈夫曼編碼後,A 是 0,B 是 10,C 是 110,D 是 111,整個字符串變爲二進制的 01000100110111,算法過程本文不再贅述。

2)Brotli 壓縮

  2015 年,Google 推出了 Brotli,這是一種全新的開源無損數據格式,並被現在所有現代瀏覽器支持。

  Brotli 也是一種基於 Deflate 算法的無損壓縮,但它會使用一個預定義的常用代碼術語詞典,該字典包含 6 種語言的 13000 多個詞。

  預定義的算法可以提升文件的壓縮比率,Brotli 能在 GZip 的基礎上,再壓縮 17-25% 的數據。

  如果瀏覽器支持 Brotli,那麼在請求首部中會將 br 令牌包含在可接受的編碼列表中,如下所示。

Accept-Encoding: gzip, deflate, br

  GZip 的壓縮級別可指定 0~9 的整數來配置,而 Brotli 的範圍是 0~11,下圖是兩種壓縮算法針對同一文件,採用不同級別的壓縮結果對比

  

  注意,使用 Brotli 壓縮所有資源非常耗費計算資源和時間,在最高壓縮級別下,會讓服務器等待動態資源。

  服務器開始發送響應所花費的時間會抵消文件大小減少帶來的任何潛在收益,也就是說會延長 TTFB 的時間。

三、HTTP/2

  HTTP/2.0 是 HTTP/1.1 的擴展版本,主要基於 Google 發佈的 SPDY 協議,引入了全新的二進制分幀層,如下圖所示。

  

  保留了 1.1 版本的大部分語義,例如請求方法、狀態碼和首部等,由互聯網工程任務組(IETF)爲 2.0 版本實現標準化。

  2.0 版本從協議層面進行改動,目標是優化應用、突破性能限制,改善用戶在瀏覽 Web 頁面時的速度體驗。

  HTTP/1.1 有很多不足,接下來列舉 4 個比較有代表性的,如下所列:

  1. 在傳輸中會出現隊首阻塞問題。
  2. 響應不分輕重緩急,只會按先來後到的順序執行。
  3. 並行通信需要建立多個 TCP 連接。
  4. 由於HTTP是無狀態的,所以每次請求和響應都會攜帶大量冗餘信息。

1)二進制分幀層

  二進制分幀層是 HTTP/2.0 性能增強的關鍵,它改變了通信兩端交互數據的方式,原先都是以文本傳輸。

  現在要先對數據進行二進制編碼,再把數據分成一個一個的幀,接着把幀送到數據流中,最後對方接收幀並拼成一條消息,再處理請求。

  在 2.0 版本中,通信的最小單位是幀(frame),若干個幀組成一條消息(message),若干條消息在數據流(stream)中傳輸。

  一個 TCP 連接可以分出若干條數據流(如下圖所示),因此 HTTP/2.0 只要建立一次 TCP 連接就能完成所有傳輸。

  

2)多路通信

  通信兩端對請求或響應的處理都是串行的,也就是按順序一個個處理。

  雖然在 HTTP/1.1 中新增了管道化的概念,讓客戶端能一下發送多個請求,減少了不必要的網絡延遲。

  不過那只是將請求的隊列順序遷移到服務器中,服務器處理還是得按順序來,所以本質上響應還是串行的。

  如果一定要實現並行通信,那麼必須建立多條 TCP 連接,多個請求分別在不同的 TCP 通道中傳輸(如下圖所示),間接實現並行通信。

  

  TCP是一種可靠的通信協議,中途如果出現丟包,發送方就會根據重發機制再發一次丟失的包。

  由於通信兩端都是串行處理請求的,所以接收端在等待這個包到達之前,不會再處理後面的請求,這種現象稱爲隊首阻塞。

  HTTP/2.0 不但解決了隊首阻塞問題,還將 TCP 建立次數降低到只要 1 次。

  通信兩端只需將消息分解爲獨立的幀,然後在多條數據流中亂序發送。

  最後在接收端把幀重新組合成消息,並且各條消息的組合互不干擾,這就實現了真正意義上地並行通信,達到了多路複用的效果。

  在CSS中,爲了減少請求次數,會把很多小圖拼在一起,做成一張大的雪碧圖,現在藉助多路通信後,不用再大費周章的製圖了,直接發請求即可。

3)請求優先級

  客戶端對請求資源的迫切度都是不同的,例如在瀏覽器的網頁(即HTML文檔)中,像 CSS、JavaScript 這些文件傳得越快越好,而像圖像則可以稍後再傳。

  在 HTTP/1.1 中,只能是誰先請求,誰就先處理,不能顯式的標記請求優先級。

  而在 HTTP/2.0 中,每條數據流都有一個 31 位的優先值,值越小優先級越大(0的優先級最高)。

  有了這個優先值,相當於能隨時建立一條綠色通道(如下圖所示),通信兩端可以對不同數據流中的幀採取不同策略,這樣能更好的分配有限的帶寬資源。

  

4)首部壓縮

  HTTP是無狀態的,爲了準確的描述每次通信,通常都會攜帶大量的首部,例如 Connection、Accept 或 Cookie。

  而這些首部每次會消耗上百甚至上千字節的帶寬。爲了降低這些開銷,HTTP/2.0 會先用 HPACK 算法壓縮首部,然後再進行傳輸。

  HPACK 算法會讓通信兩端各自維護一張首部字典表,表中包含了首部名和首部值,如下圖所示。

  

  其中首部名要全部小寫,並用僞首部(pseudo-header)表示,例如:method、:host或:path。

  每次請求都會記住已發哪些首部,下一次只要傳輸差異的數據,相同的數據只要傳索引就行。

5)服務器推送

  HTTP/2.0 支持服務器主動推送,簡單的說就是一次請求返回多個響應,如下圖所示。

  

  但是在 2022 年,開啓服務器推送的網站只有 0.7% 左右,比 2021 年還降低了 0.5% 左右。

  並且在 Chrome 106 中,默認情況下將禁用對 HTTP/2 服務器推送的支持。

  之所以如此不受待見,主要是以下兩個原因。

  • 服務器無法獲知推送的資源是否在客戶端緩存中,若存在,則用於推送資源的帶寬就會被浪費。
  • 服務端要支持推送,需要額外的開發和配置成本,這就有可能讓第三方服務器(例如 CDN 節點)無法進行推送。

  替代服務器推送的方案也已出現,第一個是第四章的預加載(preload)資源,使用 HTML 代碼,如下所示。

<link rel="preload" href="/css/style.css" as="style" />

  或者聲明 HTTP 首部(如下所示),雖然沒有主動推送快,但很安全,2022 年大約有 25% 的網頁在使用預加載。

Link: </css/style.css>; rel="preload"; as="style"

  另一個替代方案是 Early Hints,它是一個 HTTP 狀態碼(103),允許 Web 服務器在完整的 HTML 響應準備好之前告訴瀏覽器將來需要的資源。

  也就是說,瀏覽器在等待 HTML 的同時請求其他資源(例如 CSS、JavaScript、字體等),提前做一些工作,從而提升頁面加載速度。

  在下圖中,CSS 文件會在 HTML 響應完成後,再開始請求,兩者是串行的,總耗時 225ms。

  

  而在下圖中,請求 HTML 後,就會響應 103 Early Hints,告知瀏覽器去加載 CSS 文件,從而將耗時縮小到 200ms。

  

  在某些情況下,開啓 Early Hints 後,LCP 的耗時最大可以縮小 1 秒,下圖來源於 Shopify,使用的工具是 WebPageTest

  

四、HTTP/3

  在 2022 年 6 月 HTTP/3 協議實現了標準化。HTTP/3 棄用了 TCP 協議,改爲基於 UDP 的 QUIC 協議來實現。

  之所以基於 UDP,是爲了避免操作系統和中間設備(路由器、交換機等)的升級,讓新協議更容易推廣和部署。

  QUIC(快速UDP網絡連接)由 Google 於 2012 年研發,是一種可靠的網絡傳輸協議,雖然基於 UDP,但是仍然需要建立連接,並且握手也比較複雜。

  QUIC 還使用了流量和擁塞控制機制,防止發送方使網絡或接收方過載,比起 TCP 協議,QUIC 實現的這些功能更加智能,性能也更高,例如:

  • 更快的連接建立,QUIC 允許 TLS 版本協商與加密和傳輸握手同時發生,從而減少延遲。
  • 零往返時間(RTT),對於已經連接的服務器,客戶端可以直接跳過握手。
  • 更全面的加密,QUIC 使用 TLS 1.3 握手方式,默認會提供加密。

  接下來會講解 QUIC 修復的兩個 HTTP/2 重大缺陷。

1)TCP 隊首阻塞

  無論是 HTTP/1.1 還是 HTTP/2.0 都基於 TCP 協議,當在傳輸過程中出現少量的丟包時,有可能會讓整個連接中的所有流都被阻塞。

  在下圖中,第一行描述的是 HTTP/1.1,創建了 3 條 TCP 連接,分別傳輸 A、B 和 C。

  第二行描述的是 HTTP/2.0,只創建了 1 條 TCP 連接,通過不同的數據流傳輸不同的資源。

  當第 3 個數據包丟失時,需要等待重傳的新包,TCP 在此期間不會處理其餘數據。

  

  第三行描述的是 HTTP/3.0,由於數據流之間互不影響,因此除了丟包的那條數據流會被阻塞之外,其餘數據流會被繼續處理。

2)網絡切換成本

  在 TCP 中,當移動設備切換網絡時(例如從 WiFi 切換到蜂窩數據),由於 IP 地址發生了改變,因此連接就會失效,需要重連,如下圖所示。

  

  TCP 出現的比較早,目前也沒有機制允許客戶端通知服務器自己的 IP 已改變。

  QUIC 引入了一個名爲連接標識符(CID)的新概念,在兩端連接後,就會標識這個值,它具有唯一性並且在網絡切換時也不會改變。

  在下圖中,綠色方框就是 CID,有了 CID 之後就能避免重新創建連接。

  

五、CDN

  CDN(Content delivery network,內容分發網絡)可以在全球分發各類資源,包括視頻流、文本文件等。

  並根據地理位置、網絡服務商等條件,將用戶的請求定位到離他路由最短、位置最近、負載最輕的邊緣節點上,實現就近定位,以此提升性能,如下圖所示。

  

  就近訪問的能力依賴域名解析(DNS),當用戶訪問一個頁面時,瀏覽器根據域名找到對應的主機,此時就能解析到離自己最近的邊緣節點。

1)邊緣節點

  邊緣節點也稱 CDN 節點、Cache 節點等,就是在網絡上建立的邊緣服務器,對用戶具有較好的響應能力和連接速度。

  當邊緣節點收到用戶的請求後,會先從 CDN 緩存中查詢數據,若沒有找到就進行回源,回源就是向存放文件的源服務器發出請求。

  CDN 不僅能對靜態資源加速,還支持動態加速,雖然邊緣節點無法直接獲取緩存好的數據,但是可以智能選擇最佳路由進行回源。

2)邊緣計算

  CDN 中的邊緣計算(Edge Computing)是指將原來在服務器中的運算移到離用戶最近的邊緣節點中完成。

  邊緣計算能夠減緩數據爆炸、網絡流量的壓力,並且在邊緣節點處理一部分數據後,能減少設備響應時間、降低延遲。

  物聯網、AR/VR場景、大數據和人工智能等行業對邊緣計算都有着極強的需求。

3)白屏變化

  之前公司項目的部分靜態資源採取了 CDN 加速,後面讓全部資源都走 CDN,白屏時間佔比變化如下:

  • 1 秒內的佔比從 77.3% 最高提升至 78.7%
  • 1 - 2 秒佔比從 15.6% 最高提升至 18.7%
  • 2 - 3 秒佔比從 4% 最低下降至 1.8%
  • 3 - 4 秒佔比從 1.1% 最低下降至 0.6%
  • 4 秒以上的佔比從 2.1% 最低下降至 1.4%

總結

  本文從 5 個方面闡述了網絡優化的細節。

  在第一節着重講解了緩存,HTTP 的緩存分爲強緩存和協商緩存。

  在第二節對比了兩者壓縮算法:GZip 和 Brotli,並簡單介紹了 LZ77 算法和哈夫曼編碼的計算原理。

  在第三節中說明了 HTTP/2 協議的優勢,包括多路通信、請求優先級和首部壓縮,並說明了服務器推送不受歡迎的原因和替代方案。

  在第四節又說明了 HTTP/2 的缺陷,通過 HTTP/3 修復了這些缺陷,並提供了協議細節。

  在第五節中重點介紹了 CDN,包括它的原理、術語和邊緣計算的概念。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章