從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

lsvih發表於2019-02-28

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

檔案越小,意味著下載速度就越快。因此在向客戶端傳送資原始檔前,使檔案變得更小是件有益的事情。

其實,精簡與壓縮資原始檔不僅是一件很棒的事情,同時也是每一位現代開發者應該儘量去做的事情。但是,用於精簡的工具通常無法做到完美精簡;用於壓縮的壓縮器效果好壞會取決於用於壓縮的資料。下面介紹一些小技巧與方法,用於調整這些工具,使其達到最好的工作狀態。

準備工作

我們將以一個簡單的 SVG 檔案為例:

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

這個<svg>影像的內容為一個 10×10 畫素的區域(viewBox),其中包含了兩個 6×6 的正方形(<rect>)。原始檔案大小為 176 位元組,經過 gzip 壓縮過後大小為 138 位元組。

當然這個影像並沒有什麼藝術感,但它足以滿足這篇文章想要表達的意思,並且防止這篇文章變成長篇大論。

第 0 步:Svgo

執行 svgo image.svg 直接進行壓縮。

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

(為了便於閱讀,為其新增了回車與縮排)

可以明顯地看到,rect 被替換成了 pathpath 路徑形狀由它的 d 屬性定義,後面的一串命令類似於 canvas 的 draw 函式,控制一支虛擬的筆移動進行繪畫。命令可以是絕對位移(移動 x,y),也可以是相對位移(向某方向移動 x,y)。請仔細觀察其中的一條路徑:

M 0 0:路徑起點為座標(0, 0)
h 6:水平向右移動 6 px
v 6:垂直向下移動 6 px
H 0:水平移動至 x = 0
z:閉合路徑 — 移回路徑的起點

這個路徑畫出的正方形是多麼的精確!而且它比 rect 元素更加的緊湊。

另外,#f00 被改成了 red,這兒也少了一個位元組!

現在檔案大小為 135 位元組,gzip 壓縮過後為 126 位元組。

第 1 步:進行整體縮放

你可能已經注意到了,兩個路徑中的所有座標均為偶數。我們是否可以把它們都除以 2 呢?

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

影像和之前看起來是一樣的,但它縮小了兩倍。因此,我們可以對 viewBox 進行縮放,使影像與之前一樣大。

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

現在檔案大小為 133 位元組,gzip 壓縮過後為 124 位元組。

第 2 步:使用非閉合路徑

回過頭來看路徑。兩個路徑中的最後一個命令都是 z,也就是“閉合路徑”。但路徑在填充的時候會被隱式地閉合,因此我們可以刪除這些命令。

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

又少了 2 位元組,現在檔案大小為 131 位元組,gzip 壓縮過後為 122 位元組。從常識上說,原始位元組數越少,能壓縮的大小也越小。而現在我們已經在 svgo 之後節省了 4 個 gzip 位元組了。

你可能會想:為什麼 svgo 不自動進行這些優化呢?原因是縮放影像與刪除尾部的 z 命令是不安全的。請看下面的例子:

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

這是一些有 stroke(路徑寬度)的圖形。從左至右分別為:原始圖形、不閉合的情況、不閉合且進行縮放的情況。

線寬完全混亂了。慶幸的是,我們知道自己不需要使用線寬。但是 Svgo 並不知道這個情況,因此它必須要保證圖形的安全,避免不安全的變換。

現在看起來不能從程式碼中刪除任何東西了。XML 語法是嚴格的,現在所有的屬性都是必須的,並且它們的值不能不加引號。

你以為結束了?並不,這僅僅是個開始。

第 3 步:減少出現的字母

現在,讓我來介紹一個非常方便的工具:gzthermal。它可以分析需要進行 gzip 壓縮的檔案,並對進行編碼的原始位元組進行著色。更好壓縮的位元組是綠色,不好壓縮的資料是紅色,簡單明瞭。

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

請再次關注 d 屬性,尤其是被標成紅色的 M 命令值得注意。我們不能刪除它,但我們可以用相對位移 m2 2 來代替它。

初始的“指標”位置為座標軸原點(0, 0),因此移動(2, 2)和從原點移動(2, 2)是同一個意思。讓我們試試:

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小
從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

原始檔案依然是 131 位元組,但是經過 gzip 壓縮過後大小僅有 121 位元組了。發生了什麼?答案是……

哈夫曼樹(Huffman Trees)

Gzip 使用的是 DEFLATE 壓縮演算法,而 DEFLATE 演算法是以哈夫曼樹為基礎構建的。

哈夫曼編碼的核心思想就是使用更少的位元對出現次數更多的符號進行編碼,反之亦然,出現次數很少的符號需要佔用更多的位元。

沒錯,這兒說的是位元不是位元組。DEFATE 演算法會將一位元組的字元視為一系列的位元,無論一位元組包含 7、9、100 個位元,DEFLATE 演算法都能一視同仁。

以字串“Test”為例,根據它出現的字母來進行編碼:
00 T
01 e
10 s
11 t

對每個符號都進行過編碼的字串“Test”可以表示為:00011011,總共佔 8 位元。

然後我們把它開頭的“T”改成小寫“test”,再試一次:
0 t
10 e
11 s

字母 t 出現了更多的次數,它的編碼也變得更短,僅為 1 位元。這個字串經過編碼後為 010110,僅為 6 位元!


在我們的 SVG 中的 M 字母也一樣。在將其變為小寫之後,整個編碼中都不包含大寫的 M 了,可以將它從樹上移除,因此平均編碼長度可以更短。

當你編寫對 gzip 友好的程式碼時,應該更多地使用那些使用頻率較高的字元。即使你不能將程式碼長度減短,但它經過壓縮後消耗的位元數也會變少。

第 4 步:回退引用(backreferences)

DEFLATE 演算法還有一個特性:回退引用。某些編碼點不會直接進行編碼,而是告訴解碼器複製一些最近解碼的位元組。

因此,它不需要對原始位元組一次又一次地進行編碼,而是可以直接引用:
向前返回 n 個位元組,複製 m 個位元組
例如:

Hey diddle diddle, the cat and the fiddle.

Hey diddle**<7,7>**, the cat and**<12,5>**f**<24,5>**.

巧妙的是,gzthermal 還有一種只顯示回退引用的特殊模式。
gzthermal -z 會顯示以下影像:

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

普通文字位元組為橙色,可回退引用的位元組為藍色。下面的動畫更直觀:

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

除了 fill 值、m 命令和最後的 H 命令外,第二條路徑幾乎全部都使用了回退引用。對於 fill 和 m 我們無能為力,因為第二個方塊的確有著不同的顏色和位置。

但是它們的形狀是一樣的,並且我們現在對 gzip 有了更加清晰的認識。因此,我們可以將絕對位移命令 H0H2 都替換為相對位移命令:h-3

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小
從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

現在,兩個分開的回退引用合為了一個,檔案大小為 133 位元組,gzip 後的大小為 119 位元組。雖然我們在壓縮前增加了 2 個位元組,但 gzip 的結果又減少了 2 個位元組!

我們只需要關心壓縮後的大小即可:在傳送資源時,客戶端 99.9% 用的是 gzip 或者 brotli。順帶說一下 brotli。

Brotli 壓縮演算法

Brotli 是於 2015 年推出的用於替換瀏覽器中 gzip(源自 1992)的演算法。不過它與 gzip 在很多方面都有相似之處:它也是基於哈夫曼編碼與回退引用的原理,因此我們前面為 gzip 所做的調整都可以同樣利於 Brotli。最後讓我們用 Brotli 應用於前面的所有步驟:

原始檔案大小:106 位元組
在第 0 步之後(svgo):104 位元組
在第 1 步之後(viewBox):105 位元組
在第 2 步之後(使用非閉合路徑):113 位元組
在第 3 步之後(小寫 m):116 位元組
在第 4 步之後(相關命令):102 位元組

如你所見,最終的檔案比 svgo 後的更小。這可以說明,之前我們為 gzip 做的酷炫的工作同樣適用於 Brotli。

但是,中間步驟的檔案大小卻是混亂的,Brotli 壓縮後的檔案變得更大了。畢竟,Brotli 並不是 gzip,它是一種單獨的新演算法。儘管與 gzip 有一些相似之處,但仍有所不同。

其中最大的不同是,Brotli 內建了預定義字典,在編碼時使用它進行上下文啟發。此外,Brotli 的最小回退引用大小為 2 位元組(gzip 僅能建立 3 位元組及以上的回退引用)。

可以說,Brotli 比 gzip 更加難以預測。我很想解釋一下是什麼導致了“壓縮退化”,可惜 Brotli 並沒有類似於 gzip 的 gzthermal 和 defdb 之類的工具。我只能靠它的規範 以及試錯的方法來進行除錯。

試錯法

讓我們再試一次。這次將改變 fill 屬性內的顏色。顯然 red#f00 更短,但也許 Brotli 會用更長的回退引用進行壓縮。

從 Gzip 壓縮 SVG 說起 — 論如何減小資原始檔的大小

gzip 壓縮過後大小為 120 位元組,Brotli 壓縮過後為 100 位元組。gzip 流長了 1 位元組,Brotli 流短了 2 位元組。

此時,它在 Brotli 中表現更好,在 gzip 中表現更差。我覺得,這完全無礙!因為我們幾乎不可能一次性將資料針對所有壓縮器進行優化,並得到最佳結果。解決壓縮器問題就像轉一個糟糕的魔方,只能儘量優化。

總結

上面描述的所有的調整方法都不僅限於 SVG 壓縮為 gzip 的情景。

以下是一些可以幫助你寫出更具備壓縮效能的程式碼的準則:

  1. 壓縮更小的源資料可能會得到更小的壓縮資料。
  2. 不同的字元越少就意味著熵越少。而熵越小,壓縮效果就越好。
  3. 頻繁出現的字元會以更小的位元組被壓縮。刪除不常見字元以及使常見字元更常見可以提高壓縮效率。
  4. 長段重複的程式碼可以被壓縮成幾個位元組。DRY(“不要重複自己”原則)不一定在任何情況下都是最好的選擇,有時候重複自己反而能得到更好的結果。
  5. 有些時候更大的源資料反而可以得到更小的壓縮資料。減少熵可以讓壓縮器更好地移除冗餘的資訊。

你可以在 此 GitHub repo 中找到以上所有資源、壓縮過的圖片以及其它資料。

希望你喜歡這篇文章。下次我們將討論如何壓縮普通 JavaScript 程式碼與 Webpack bundle 中的 JavaScript 程式碼。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章