前端效能優化gzip初探(補充gzip壓縮使用演算法brotli壓縮的相關介紹)

wangzy2019發表於2019-07-12

通常在看一些面試題問到前端有哪些效能優化手段的時候,可能會提到一個叫做gzip壓縮的方法。正好最近在學習node檔案流操作和zlib模組的時候,對gzip壓縮有了一個新的認識。今天就和大家一起分享一下,gzip是什麼,從瀏覽器請求到收到服務端資料發生了什麼。

由於之前的題目《你知道前端效能優化gzip的工作原理嗎?》有些歧義,我本來想說的工作原理是這個過程中發生了什麼,而很多點進來的大佬想看到的是壓縮演算法實現。所以將標題改為《前端效能優化gzip初探》,可能後續會對gzip壓縮實現的一些粗淺的認識補上。小弟不甚惶恐,請大家見諒。

什麼是gzip

兄弟你聽說winRAR嗎?聽說過360壓縮,快壓,好壓嗎?都聽說過,那你聽過GNUzip嗎?

對,沒有錯,gzip就是GNUzip的縮寫,也是一個檔案壓縮程式,可以將檔案壓縮排字尾為.gz的壓縮包。而我們前端所講的gzip壓縮優化,就是通過gzip這個壓縮程式,對資源進行壓縮,從而降低請求資源的檔案大小。

gzip壓縮優化在業界的應用有多麼普遍呢,基本上你開啟任何一個網站,看它們的html,js,css檔案都是經過gzip壓縮的(即使js,css這類檔案經過了混淆壓縮之後,gzip仍然可以明顯的優化檔案體積。)。

Tips:通常gzip對純文字內容可壓縮到原大小的40%。但png、gif、jpg、jpeg這類圖片檔案並不推薦使用gzip壓縮(svg是個例外),首先經過壓縮後的圖片檔案gzip能壓縮的空間很小。事實上,新增標頭,壓縮字典,並校驗響應體可能會讓它更大。

比如現在,你正在訪問的掘金,開啟除錯工具,在網路請求Network中,選擇一個js或css,都能在Response Headers中找到 content-encoding: gzip 鍵值對,這就表示了這個檔案是啟用了gzip壓縮的。

前端效能優化gzip初探(補充gzip壓縮使用演算法brotli壓縮的相關介紹)

gzip壓縮過程

上面我們可以看到,這裡是掘金網站引入的一個growingIO資料分析的檔案,經過了gzip壓縮,大小是25.3K。現在我們把這個檔案下載下來,建一個沒有開啟gzip的本地伺服器,看看未開啟gzip壓縮這個檔案是多大(其實下載下來就已經能看到檔案大小了,是88.73k)。

此處我們用原生node寫一個服務,便於我們學習理解,目錄和程式碼如下:

前端效能優化gzip初探(補充gzip壓縮使用演算法brotli壓縮的相關介紹)

const http = require("http");
const fs = require("fs");

const server = http.createServer((req, res) => {
  const rs = fs.createReadStream(`static${req.url}`); //讀取檔案流
  rs.pipe(res); //將資料以流的形式返回
  rs.on("error", err => {
    //找不到返回404
    console.log(err);
    res.writeHead(404);
    res.write("Not Found");
  });
});
//監聽8080
server.listen(8080, () => {
  console.log("listen prot:8080");
});

複製程式碼

node server.js啟動服務,此時我們訪問http://localhost:8080/vds.js,網頁會顯示vds.js檔案的內容,檢視Network面版,會發現vds.js請求大小是88.73k,和原始資原始檔大小一致,Response Headers中也沒有 content-encoding: gzip ,說明這是未經過gzip壓縮的。

前端效能優化gzip初探(補充gzip壓縮使用演算法brotli壓縮的相關介紹)

如何開啟gzip呢,很簡單,node為我們提供了zlib模組,直接使用就行,上面的程式碼簡單修改一下就可以。

const http = require("http");
const fs = require("fs");
const zlib = require("zlib"); // <-- 引入zlib塊

const server = http.createServer((req, res) => {
  const rs = fs.createReadStream(`static${req.url}`);
  const gz = zlib.createGzip(); // <-- 建立gzip壓縮
  rs.pipe(gz).pipe(res); // <-- 返回資料前經過gzip壓縮
  rs.on("error", err => {
    console.log(err);
    res.writeHead(404);
    res.write("Not Found");
  });
});

server.listen(8080, () => {
  console.log("listen prot:8080");
});

複製程式碼

執行這段程式碼,訪問http://localhost:8080/vds.js,會發現網頁沒有顯示vds.js內容,而是直接下載了一個vds.js檔案,大小是25k,大小好像是經過了壓縮的。但是如果你嘗試用編輯器開啟這個檔案,會發現開啟失敗或者提示這是一個二進位制檔案而不是文字。這個時候如果反應快的朋友可能會和我第一次的想法一樣,試試把js字尾改成gz。因為前面說了,其實gzip就是一個壓縮程式,將檔案壓縮排一個.gz壓縮包。這個地方會不會其實是一個gz壓縮包?

不賣關子了,將字尾名改為gz,解壓成功後會出來一個88.73k的vds.js。

前端效能優化gzip初探(補充gzip壓縮使用演算法brotli壓縮的相關介紹)

相信到了這裡大家都應該豁然開朗,原來gzip就是將資原始檔壓縮排一個壓縮包裡啊,但是唯一的問題是這壓縮包我怎麼用,我請求一個檔案,伺服器你卻給我一個壓縮包,我識別不了啊。

解決這個問題更簡單,服務端返回壓縮包的時候告訴瀏覽器一聲,這其實是一個gz壓縮包,瀏覽器你使用前先解壓一下。而這個通知就是我們之前判斷是否開啟gzip壓縮的請求頭欄位,Response Headers裡的 content-encoding: gzip

我們最後修改一下程式碼,加一個請求頭:

const http = require("http");
const fs = require("fs");
const zlib = require("zlib"); 

const server = http.createServer((req, res) => {
  const rs = fs.createReadStream(`static${req.url}`);
  const gz = zlib.createGzip(); 
  res.setHeader("content-encoding", "gzip"); //新增content-encoding: gzip請求頭。
  rs.pipe(gz).pipe(res); 
  rs.on("error", err => {
    console.log(err);
    res.writeHead(404);
    res.write("Not Found");
  });
});

server.listen(8080, () => {
  console.log("listen prot:8080");
複製程式碼

此時瀏覽器再請求到gzip壓縮後的檔案,會先解壓處理一下再使用,這對於我們使用者來說是無感知的,工作瀏覽器都在背後默默做了,我們只是看到網路請求檔案的大小,比伺服器上實際資源的大小小了很多。

這一段花了很長的篇幅來講gzip的工作原理,明白之後其實真的很簡單,而且以後問到前端效能優化這一點,相信gzip這條應該是不會忘了的。

gzip的注意點

前面說的哪些檔案適合開啟gzip壓縮,哪些不適合是一個注意點。

還有一個注意點是,誰來做這個gzip壓縮,我們的例子是在接到請求時,由node伺服器進行壓縮處理。這和express中使用compression中介軟體,koa中使用koa-compress中介軟體,nginx和tomcat進行配置都是一樣的,這也是比較普遍的一種做法,由服務端進行壓縮處理。

伺服器瞭解到我們這邊有一個 gzip 壓縮的需求,它會啟動自己的 CPU 去為我們完成這個任務。而壓縮檔案這個過程本身是需要耗費時間的,大家可以理解為我們以伺服器壓縮的時間開銷和 CPU 開銷(以及瀏覽器解析壓縮檔案的開銷)為代價,省下了一些傳輸過程中的時間開銷。

如果我們在構建的時候,直接將資原始檔打包成gz壓縮包,其實也是可以的,這樣可以省去伺服器壓縮的時間,減少一些服務端的消耗。

比如我們在使用webpack打包工具的時候可以使用compression-webpack-plugin外掛,在構建專案的時候進行gzip打包,詳細的配置使用可以去看外掛的文件,非常簡單。

補充內容:gzip檔案分析

開頭曾經提到過gzip是一個壓縮程式而並不是一個演算法,經過gzip壓縮後檔案格式為.gz,我們對.gz檔案進行分析。

使用node的fs模組去讀取一個gz壓縮包可以看到如下一段Buffer內容:

const fs = require("fs");

fs.readFile("vds.gz", (err, data) => {
  console.log(data); // <Buffer 1f 8b 08 00 00 00 00 00 00 0a  ... >
});

複製程式碼

通常gz壓縮包有檔案頭,檔案體和檔案尾三個部分。頭尾專門用來儲存一些檔案相關資訊,比如我們看到上面的Buffer資料,第一二個位元組為1f 8b(16進位制),通常第一二位元組為1f 8b就可以初步判斷這是一個gz壓縮包,但是具體還是要看是否完全符合gz檔案格式,第三個位元組取值範圍是0到8,目前只用8,表示使用的是Deflate壓縮演算法。還有一些比如修改時間,壓縮執行的檔案系統等資訊也會在檔案頭。

而檔案尾會標識出一些原始資料大小的相關資訊,被壓縮的資料則是放在中間的檔案體。

前面所說的,對於已經壓縮過的圖片,開啟了gzip壓縮反而可能會使其變得更大,就是因為中間實際壓縮體沒怎麼減小,但是卻新增了頭尾的壓縮相關資訊。

補充內容:gzip的壓縮演算法

gzip中間的檔案體,使用的是Deflate演算法,這是一種無失真壓縮解壓演算法。Deflate是zip壓縮檔案的預設演算法,7z,xz等其他的壓縮檔案中都有用到,實際上deflate只是一種壓縮資料流的演算法. 任何需要流式壓縮的地方都可以用。

Deflate演算法進行壓縮時,一般先用Lz77演算法壓縮,再使用Huffman編碼。

Lz77演算法的原理是,如果檔案中有兩塊內容相同的話,我們可以用兩者之間的距離,相同內容的長度這樣一對資訊,來替換後一塊內容。由於兩者之間的距離,相同內容的長度這一對資訊的大小,小於被替換內容的大小,所以檔案得到了壓縮。

舉個例子:

http://www.baidu.com https://www.taobao.com

上面一段文字可以看到,前後有部分內容是相同的,我們可以用前文相同內容的距離和相同字元長度替換後文的內容。

http://www.baidu.com (21,12)taobao(23,4)

Deflate採用的Lz77演算法是經過改進的版本,首先三個位元組以上的重複串才進行偏碼,否則不進行編碼。其次匹配查詢的時候用了雜湊表,一個head陣列記錄最近匹配的位置和prev連結串列來記錄雜湊值衝突的之前的匹配位置。

而Huffman編碼,因為理解的不是很清楚,這裡就不便多說了,只大概瞭解是通過字元出現概率,將高頻字元用較短位元組進行表示從而達到字串的壓縮。

其實簡單的看一下這些演算法,我們大概能明白,為什麼js,css這些檔案即使經過了工具的混淆壓縮,通過gzip依然能得到可觀的壓縮優化。

更多的gzip演算法內容可以閱讀下面的文章:

GZIP壓縮原理分析系列

補充內容:brotli壓縮

感謝wangyjx1的評論,特別去了解了一下brotli壓縮,這裡將瞭解到的一些內容分享出來。

Brotli由google在2015年推出,用於網路字型的離線壓縮,後釋出包含通用無損資料壓縮的Brotli增強版本,Brotli基於LZ77演算法的一個現代變體、Huffman編碼和二階上下文建模。

與常見的通用壓縮演算法不同,Brotli使用一個預定義的120千位元組字典。該字典包含超過13000個常用單詞、短語和其他子字串,這些來自一個文字和HTML文件的大型語料庫。預定義的演算法可以提升較小檔案的壓縮密度。使用brotli取代deflate來對文字檔案壓縮通常可以增加20%的壓縮密度,而壓縮與解壓縮速度則大致不變。

目前該壓縮方式大部分瀏覽器(包括移動端)新版本支援良好,詳細的支援情況可在caniuse查詢到。

支援Brotli壓縮演算法的瀏覽器使用的內容編碼型別為br,例如以下是Chrome瀏覽器請求頭裡Accept-Encoding的值:

Accept-Encoding: gzip, deflate, sdch, br

如果服務端支援Brotli演算法,則會返回以下的響應頭:

Content-Encoding: br

Tips:brotli 壓縮只能在 https 中生效,因為 在 http 請求中 request header 裡的 Accept-Encoding: gzip, deflate 是沒有 br 的。

目前該壓縮方案的使用情況,去檢視了幾大網站的網路請求,國外的google,facebook,bing都已用上了Brotli壓縮。國內的話淘寶,百度,騰訊,京東,b站幾個大站基本都沒有使用,唯一我發現使用了brotli壓縮你們猜是哪個網站?是知乎,果然有逼格,掘金可以考慮跟上了。好在騰訊雲,阿里雲,又拍雲這類的cdn加速服務商都支援了brotli壓縮。

node中沒有原生模組支援brotli壓縮,可以使用第三方庫來支援,比如iltorb,感興趣的朋友可以自己嘗試一下(反正新瞭解的東西,我肯定是要親自動手搞一下才行)。

最後

十分抱歉之前的標題和內容讓大家產生誤解,掘金的大佬們真的很嚴格。這文的寫作緣由只是因為學習node中理解了gzip的工作過程,想分享給大家。沒敢講壓縮演算法這塊,是因為這地方本身還要學習,而且對於前端來說不是必須掌握的知識點,面試應該不會問這麼深,當然感興趣的可以自己瞭解。但是既然大家希望能提到這些,我就獻醜簡單分享一下我知道的東西,有不足之處歡迎大家批評指正。

文章列表

相關文章