深挖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: , , ,

支付安全感的设计思考
上一遍
腾讯品牌体验设计
下一遍
19,538
QQ空间 新浪微博 Facebook Google+

相关推荐

留下你的想法吧