Web 性能优化(3)——探讨 data URI 的性能

业内常会有 Data URI 的利与弊、用与不用的讨论,即使在最有经验的前端开发者眼中,也会形成对 data URI 截然不同的看法。

什么是 Data URI

减少 Request,一直是业内公认的一项优化网站加载的方案。过去,对于众多图标类的小图片,通常都会采用雪碧图的方法合并多张小图片为一张大图片、来减少请求。现在,data URI 成为了新的选择。

RFC2397 中首次定义了 data URI 的规范和格式如下:

1
data:[<mime type>][;charset=<charset>][;base64],<encoded data>

在这种格式中,data:就是 URI 的协议,表明这是一个 data URI。
mime type可能是 image/png 之类的,如果不填,默认是 text/plain

性能神器 还是弃之可惜的鸡肋

节省请求全等于优化性能?

首先,如果直接在 <img> 标签的 src 中使用 base64 时,图片出现很多次时就会需要把 base64 图片的文本内容重复很多次,导致 HTML 变大。虽然多次重复的内容很适合 Gzip,但对于浏览器来说,导入 HTML 并生成 DOM 则会被阻碍。而且多次重复的 data URI 浏览器会不断的暂停渲染和进行解码,如果使用 data URI 的文件过多过大,就会阻碍页面的渲染。
当然,你可以说,在 style.css 里写好 background 可以应对这个问题。
固然,将 data URI 写进 CSS 以后,似乎是减少了请求。但是这样做的缺点就没那么容易发现了。样式表会变得很大,从而阻塞关键下载和渲染。通俗地讲,图片文件的体积被转移到了 HTML 或 CSS 中,而后者的体积直接影响渲染,导致用户会长时间注视空白屏幕。HTML 和 CSS 阻塞渲染,图片不会。

这是用户打开网页时浏览器加载页面的过程:

  • 下载 HTML 文档。HTML 内容准备就绪后,浏览器解析字节并构建 DOM 树。
  • 在浏览器构建我们这个简单页面的 DOM 时,在文档的 head 部分遇到了一个 link 标签,该标记引用一个外部 CSS 样式表 style.css。由于预见到需要利用该资源来渲染页面,它立即发出了对该资源的请求。
  • 与处理 HTML 时一样,我们需要将收到的 CSS 转换成浏览器能够理解和处理的东西。因此浏览器会重复解析过程,不过是为解析 CSS,而不是 HTML。它需要提取并解析 CSS 文件以构建页面。
  • 在浏览器构建页面时,如果遇到了标签,它意识到需要该资源来渲染页面,就会把该资源加入到请求队列。但是图片的暂时缺失不影响浏览器渲染其他部分。因此图片不会阻塞关键路径渲染。

0000089.png

Gzip 能缓解这一切?

Gzip 又是什么?

Gzip 是在 Web 端最常用的一种压缩文本的方法。Gzip 压缩算法分两步。第一步,采用LZ77 算法的一个变种替换字符串,第二步,使用Huffman 树来储存出现的位置和长度。

看不懂?我也看不懂。。不过我找到了下面这张图,这样就形象多了。

0000090.png

HTML 中重复出现大量的 HTML 标签以及类名等,CSS 中重复出现大量的属性,JavaScript 中重复的函数调用等(即使经过混淆)。因此 HTML、CSS、JavaScript 的 Gzip 压缩率都是很高的。但是由于 base64 近乎于乱码的文本是无规律的,所以在 Gzip 中不达不到较高的压缩率。

考虑考虑缓存?

如果我们把样式、图片文件合并到变成一个资源,我们就无法再分别为它们配置缓存时间,以及更新资源。而图片、HTML 和 CSS 的更新频率都是不一样的。
然而 CSS 文件的修改频率还算是较高的,图片其次。我们一般会为不同类型的文件利用缓存头设置不同的缓存失效时间,以及在更新某个文件之后单独更新这个文件的时间戳。但是混在一起之后,即使我们只是想更新CSS规则里面一个字号,整个巨大的文件就会重新生成。用户不得不在每次更新后重新下载整个大文件,这违背了基本的缓存原则。


总结一下:

  • base64 会让样式文件变得很大而阻塞关键下载和渲染
  • css 因 base64 增加的体积无法通过 Gzip 很好地压缩
  • 浏览器渲染方面,增加了解析 css 的耗时
  • 在 css 文件中过多使用 base64 会让首次渲染时间大幅增加,移动端影响可能更大

Data URI 的实践

在 hexo-theme-material 1.4.0 版本的开发中为了优化页面加载,我们开始考虑应用 data URI。
由于 Material 主题 1.4.0 版本的前期开发中已经引进了一套基于 localstorage 的缓存方案,不怕强制刷新和禁用缓存,极大程度上优化了二次加载。
在这基础上,我将 footer sns 的 icon 独立了出来,使用 base64 加码,并独立存储在一个 css 当中。这个 css 便不需要经常更新。

Material 主题的 footer sns icon 是 svg。svg 的格式是:

1
2
3
<svg xmlns="http://www.w3.org/2000/svg" viewBox= ...>
<path ...></path>
</svg>

不同的 svg 中,只有定义图像的 <path> 部分会有所不同,开头定义规范的部分之类的都完全相同,所以这一部分也是可以被 Gzip 压缩的,一定程度上缓解了传输文件大小的问题。

在实际测试中,直接加载外链的 svg,即使用了支持 HTTP2 的 CDN 可以连接多路复用,加载一个 svg 仍然需要 25ms,而且这一部分是最后加载的,直接影响了 DOMContentLoaded 的触发。采用该方案以后,DOMContentLoaded 提前了将近 15ms 左右触发,效果虽然不明显,但是说明这个方案毕竟是可行的。