深挖data URI性能瓶頸

錢德勒

Data URI是一個富有爭議的特性。即使在最有經驗的前端開發者眼中,也會形成對 data URI 截然不同的看法:有人認為它是性能優化神器,有人認為它已經落後於時代。為什麼會這樣?本文帶你進行深入的剖析。

URI,不是URL

我們習慣的 URL 的全稱是統一資源定位符(uniform resource locator),它是由一個“協議”和一個“地址”組成。協議告訴瀏覽器或者程序用何種方式去獲取這個資源,地址告訴程序在哪裡找到這個資源,每個地址都能唯一定位一個公開資源(比如圖片、HTML、JavaScript 等)或非公開資源(這時候就需要提供用戶名和密碼)。

URI 是一個更廣的概念,或者說 URL 是最常見的一種 URI。URI的全稱是統一資源定位符(uniform resource identifier),由一個“協議”和“定位符”組成。定位符其實就是補充信息,它可以是一個地址(如果是這樣的話,那這個 URI 就是一個 URL),也可以是數據本身(比如 data URI),或者命名空間(URN)。

所以 Data URI 不是 URL。

在1998年的RFC 2397中第一次定義了 Data URI:

A new URL scheme, "data", is defined. It allows inclusion of small data items as "immediate" data, as if it had been included externally.

本文檔定義了一個新的URL 協議(我覺得這裡有點誤用,應該是 URI 協議,因為跟蒂姆·伯納斯·李的RC 2396有衝突)。它允許(文檔)直接使用一小段數據作為“即時數據”,而不是之前那樣必須引用外部資源。

隨後,文檔定義了 data URI 的格式:

data:[<mediatype>][;base64],<data>

在這種格式中,data:就是 URI 的協議,表明這是一個 data URI。

mediatype可能是image/png之類的,如果不填,默認是text/plain

以下是一個HTML代碼片段:

<IMG SRC="data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7" ALT="Larry">

Base64 編碼

可能有同學會問,base64 編碼在 data URI 中的角色是什麼?

我們一般指定base64編碼方式,如果不填,默認是低效的URL編碼

對於非英文字符串,URL 編碼一種非常浪費空間的編碼方式。URL 編碼在地址欄中很常見,對於 URL 安全的字符(比如英文字母、數字、中劃線、下劃線等)就直接顯示,對於 URL 不安全的字符(比如非英文的字符)就編碼成%xx的形式。

二進制文件中包含很多 URL 不安全的字符,所以轉成 URL 編碼字符之後很冗長。所以有了 base64 編碼,base64是一種基於64個可打印字符來表示二進制數據的表示方法。由於2的6次方等於64,所以每6個比特為一個單元,對應某個可打印字符(包括大寫的英文字母、小寫的英文字母、數字、+、/)。

以下是“MAN”這個單詞對應的二進制位和 base64 編碼。

舉一個實際的例子,對於下面這個圖片:

直接使用二進制文件,然後進行 URL 編碼結果如下(空格會被忽略):

data:image/gif,GIF89a%22%00%1B%00%F7%00%00lll%D6%D6%D6%FF%EB%85%FF%E0%7B%FF%F7%91%FF%D4o%DF%DF%DF%F6%F6%F6%87%87%87%FE%CBf%FF%F4%8E%E6%B3NKKK%C5%92-%FF%FF%99%FF%FF%FF%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%2C%00%00%00%00%22%00%1B%00%00%08%A9%00%1F%08%1CH%B0%A0%C1%83%08%13%5C%C8%B0%A1%C3%87%10%23J%9CH%91%60%83%8B%0D%0C%1C%A8h%B0%81%C5%00%1B9%0A%F4%E8%A0%A4%83%07%181j%9C%D8%80%80%82%97%2F%0B6%40%60%80%A5%00%01)s%AA%94%D8%60%80G%84%02P%22%E0Y%A0%81%C9%A3%25%138h%00%80g%02%A3%04%A2J%8D%BA%60i%D3%88%0D%9E%3A%B8%C9%95kU%A6N%8D%0E%18Kv%EC%D7%AB%10%B3%1A-%C0%B6-%5B%A3%60%23%1A%D0I%97%C1%D0%88%07%02%20%00%C0%B7%AF%00%08%02L%3C%60%20%80%E1%C3%88%03%AC%14%C9%B8%B1%E3%C7%90%23K%9EL0%20%00%3B

如果是二進制文件,按比特位轉化成 base64 編碼,結果如下(空格可忽略):

data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7

巨大的優勢!Base64 編碼明顯比 URL 編碼小很多(但是因為使用了6個比特而不是8個比特,所以仍然比壓縮過的二進制文件大一些)。

因此,當我們提到 data URI 時,99%同時是指配套使用 base64 編碼技術,來把一個二進制資源文件(比如字體或圖片)合併到主文檔(可能是HTML,可能是CSS)中。

性能神器還是棄之可惜的雞肋?

在一次面試中,我問一個候選人圖片優化有哪些方法,他說,可以用 base64(data URI)。我深入問 base64 有什麼優缺點,用在什麼場合,他提到,在img標籤的 src 中使用 base64 時,如果圖片出現很多次,就會需要把base64 圖片的文本內容重複很多次,導致 HTML 變大,如果是在CSS文件中通過background的方式來引用 base64 圖片,就不會有這個問題。

其實這只是“不要重複你自己原則”(DRY原則)的一個應用,談不上性能優化。可能他覺得 base64 是一個較少見的技術,所以說出來肯定比較厲害。其實不然,下面就來深挖一下 data URI 的性能優劣。

誤區一:節省請求等於優化性能?

原本頁面由 HTML、CSS、和若干圖片組成,如果將圖片通過 data URI 的形式合併到CSS文件中,頁面就可以完全由 HTML、CSS 來組成。

對於前端來說,顯而易見好處是能夠減少一個圖片的 HTTP 請求,而缺點可能就不夠顯而易見。

樣式表會變得很大,從而阻塞關鍵下載和渲染。通俗地講,圖片文件或字體文件的體積轉移到了 HTML 或 CSS中,而後者的體積直接影響渲染,導致用戶會長時間注視空白屏幕。HTML 和 CSS 阻塞渲染,圖片不會。

用戶在打開一個網頁時經歷這幾個主要階段分解:

  1. 下載HTML文檔。HTML 內容準備就緒後,瀏覽器解析字節、將其轉換為令牌,並構建 DOM 樹。
  2. 在瀏覽器構建我們這個簡單頁面的 DOM 時,在文檔的 head 部分遇到了一個 link 標記,該標記引用一個外部 CSS 樣式表:style.css。由於預見到需要利用該資源來渲染頁面,它立即發出了對該資源的請求。
  3. 與處理 HTML 時一樣,我們需要將收到的 CSS 規則轉換成某種瀏覽器能夠理解和處理的東西。因此,瀏覽器會重複解析過程,不過是為解析CSS,而不是 HTML。它需要提取並解析 CSS 文件以構建 CSSOM,然後使用 DOM 和 CSSOM 來構建呈現樹
  4. 在瀏覽器構建頁面時,如果遇到了<img>標籤,它意識到需要該資源來渲染頁面,就會把該資源加入到請求隊列。但是圖片的暫時缺失不影響瀏覽器渲染其他部分。因此圖片不會阻塞關鍵路徑渲染。
關鍵路徑通暢 vs 關鍵路徑阻塞

關鍵路徑通暢 vs 關鍵路徑阻塞

這就是Base64的第一個缺點,資源合併到CSS文件中導致體積增大,進而阻塞關鍵路徑。

誤區二: Base64 能獲益於 Gzip 壓縮?

有人會說,雖然 CSS 文件變大了,但現在整個 CSS 文件都能Gzip壓縮了呀。事實真的如此嗎?

Gzip是在Web端最常用的一種壓縮文本的方法。

Gzip壓縮算法分兩步。第一步,採用LZ77算法的一個變種替換字符串,第二步,使用Huffman樹來儲存出現的位置和長度。

The deflation algorithm used by gzip (also zip and zlib) is a variation of LZ77. It finds duplicated strings in the input data. The second occurrence of a string is replaced by a pointer to the previous string, in the form of a pair (distance, length).

簡單來講,Gzip把原文本中多次出現的相同字符串記為一個“標記”,所以文本中重複出現的字符串越多,壓縮率越高。

gzip壓縮原理

gzip壓縮原理

HTML 中重複出現大量的 HTML 標籤以及類名等,CSS中重複出現大量的屬性,JavaScript 中重複的函數調用等(即使經過混淆)。因此 HTML、CSS、JavaScript 的 Gzip 壓縮率都是很高的,最高可達到90%。

而圖片經過Base64轉化後變成的文本是無規律的,所以在Gzip中不能達到較高的壓縮率。

事實證明了這一點:

base64文本gzip壓縮率較低

普通 CSS 文件有90%的壓縮率,加入 Base64 後的 CSS 文件壓縮率降到了74%,壓縮後體積從68K增加到232K。

加上CSS阻塞渲染這一點,任何理智的人都應該把這額外的164K資源挪到外面,一個不阻塞渲染的地方。

誤區三:考慮緩存了嗎?

Base64影響了我們的緩存策略。我們把樣式、圖片、字體文件等合併到一起之後,整個變成一個資源,我們無法再分別為它們配置緩存時間,以及更新資源。而圖片、字體、HTML 和 CSS 的更新頻率都是不一樣的。

在平常的項目中,CSS文件的修改頻率是較高的,圖片其次,而字體文件,幾乎是幾個月甚至一年以上才修改一次。我們一般會為不同類型的文件設置不同的緩存失效時間,以及在更新某個文件之後單獨更新這個文件的時間戳。

混在一起之後,即使我們只是想更新CSS規則裡面一個字號,整個幾百K的文件就會重新生成。用戶不得不在每次小型更新後重新下載整個大文件,這違背了基本的緩存原則。

Base64跟CSS混在一起,難以分別進行緩存設置和更新。

誤區四:CSSOM 渲染

Base64跟CSS混在一起,大大增加了瀏覽器需要解析CSS樹的耗時。其實解析CSS樹的過程是很快的,一般在幾十微妙到幾毫秒之間。如果CSS文件中混入了Base64,那麼(因為文件體積的大幅增長)解析時間會增長到十倍以上。

請再一次注意,增加的解析時間全部都在關鍵渲染路徑上。

CSS解析過程

通過數據實驗證明,單獨解碼Base64圖片會比單獨解碼jpg圖片快一點點,但是綜合看來,由於解析CSS文件花了太久,Base64方案的CSSOM耗時還是慢很多。

有沒有適合使用 data URI 的場景?

說了這麼多缺點,有沒有適合使用 data URI 的項目呢?

有!在某些罕見特例之下,也許使用Base64是合理的選擇。

例如,對於僅有一兩個小圖標的網頁,開發者也許沒有必要專門生成一個雪碧圖。比如維基百科邊欄的小圖標。

維基百科的小圖標適合使用base64

維基百科的小圖標適合使用base64

如果這個網頁還能保證幾個月不會更新,那麼緩存不可控的問題也不會凸顯。比如我的個人博客就使用了 data URI 來顯示一個小圖標。

最後,對於一個使用了背景平鋪圖片的網頁,平鋪圖片無法合併到頁面資源雪碧圖中,這時使用 data URI 也許是一個合理的選擇。

即使如此,開發者仍需注意,隨着項目的增長和變更,也許 data URI 不會總是最合理的選擇。比如一個圖標會變成兩個、三個(如果圖標數量再多一點,更好的選擇是把這些圖標合併起來);幾個月更新一次的網頁變成幾天更新一次等。

所以每當項目有比較大的變化時,都應該重新評估 data URI 的優缺點。

總結

  1. Base64會讓樣式文件變得很大,從而阻塞關鍵下載和渲染。
  2. 樣式文件增加的體積無法通過Gzip很好地壓縮。
  3. 在緩存方面,本可以分別設置緩存策略的圖片和樣式表也混在一起,無法區別更新。
  4. 在瀏覽器渲染方面,也增加了解析CSS樹的耗時。
  5. 在CSS文件中過多使用Base64時,會讓首次渲染時間(First Paint)增加2倍以上,在移動端,由於網絡和手機性能的緣故,這一時間可能會增加10倍以上。

對於 data URI,前端開發者需要謹慎使用,並注意到它的優缺點,以獲得更好的性能。也許在下一個項目中你仍然不會使用 data URI,但至少下一次面試中你有更多可以說的了,不是嗎?

參考資料:

https://zh.wikipedia.org/zh-cn/Base64

https://csswizardry.com/2017/02/base64-encoding-and-performance/

https://csswizardry.com/2017/02/base64-encoding-and-performance-part-2/

http://www.gzip.org/algorithm.txt

https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/timeline-tool

Tags: , , ,

錢德勒
支付安全感的設計思考
上一遍
騰訊品牌體驗設計
下一遍
9,708
QQ空間 新浪微博 Facebook Google+

相關推薦

留下你的想法吧