data URI 的代价
data URI 把文件放进 URL 中:data:image/png;base64,iVBORw0KGgo...。凡是能用 URL 的地方都能用它,所以图标可以直接写在 HTML 的 src 属性或 CSS 的 background-image 中,浏览器不必为它单独发请求。
需要权衡的有:
- 体积。Base64 会让载荷变成原来的 4/3,大约增大 33%(精确公式见 Base64 体积开销)。
data:image/png;base64,前缀还要再加 22 个字节。 - 缓存。内联的文件只能作为所在文档的一部分被缓存。内联在每个 HTML 页面中的 logo 会随每个页面重新下载。内联在 CSS 文件中时,它和 CSS 一起缓存,改动一个图标就会让整个样式表的缓存失效。
- 渲染。在阻塞渲染的 CSS 到达之前,浏览器什么都画不出来。如果样式表里内联了 200 KB 的字体或图片,首次渲染就会推迟这些字节下载所需的时间。在 HTML 中,内联数据会把它后面的内容在字节流中往后推。
- 无法懒加载。给使用 data URI 的
<img>加loading="lazy"毫无作用,因为字节已经随页面一起到达了。
相应的好处是省掉了一次请求。在 HTTP/1.1 下这很重要,因为浏览器对每个主机只开很少几个连接。到了 HTTP/2 和 HTTP/3,许多请求共用一个连接,节省就小得多了:主要是请求头和响应头,以及浏览器较晚发现的文件所需的一次往返延迟。
实测:gzip 和 Brotli 对 Base64 的影响
33% 这个数字针对的是未压缩的文本。大多数 HTML 和 CSS 都以 gzip 或 Brotli 传输,所以真正重要的是压缩后的大小。我们自己测了一下。
方法。样本文件取自 Debian 13 的软件包:一个 16×16 的 PNG 图标(HighContrast 图标主题)、一个 48×48 的 RGBA PNG(hicolor 主题)、2880×1800 的 Xfce 蓝色壁纸 JPEG、用 Pillow 以质量 80 从中缩小得到的 800×500 JPEG、Roboto Slab Regular WOFF2 字体,以及两个 SVG 图标(一个 479 字节的 Adwaita symbolic 图标和 7.6 KB 的 GNOME Debian 漩涡标志)。每个文件及其 Base64 编码(只有载荷,不含 data: 前缀)都用 Node.js 24 的 zlib 压缩:gzip 级别 9、Brotli 质量 11,这是预压缩静态资源的常用设置。单位为字节。
| 文件 | 原始 | Base64 | 原始 gzip | Base64 gzip | 原始 Brotli | Base64 Brotli |
|---|---|---|---|---|---|---|
| PNG 16×16 | 281 | 376 | 285 | 306 | 285 | 284 |
| PNG 48×48 | 4,552 | 6,072 | 4,575 | 4,602 | 4,551 | 4,589 |
| JPEG 800×500 | 8,195 | 10,928 | 7,425 | 7,173 | 7,160 | 6,908 |
| JPEG 2880×1800 | 197,011 | 262,684 | 182,684 | 183,945 | 180,114 | 181,140 |
| WOFF2 字体 | 52,532 | 70,044 | 52,538 | 52,765 | 52,536 | 52,690 |
| SVG 图标 | 479 | 640 | 291 | 427 | 247 | 410 |
| SVG 漩涡 | 7,610 | 10,148 | 3,123 | 4,398 | 2,582 | 3,823 |
这些数字说明:
已压缩的二进制文件几乎完全抵消了开销。PNG、JPEG 和 WOFF2 的数据本来就是压缩过的,接近随机,再压缩一次收益很小(两个 PNG 在 gzip 后甚至变大了一点)。随机字节的 Base64 使用 64 个符号,每个占 8 比特,而 Huffman 编码会把它压回到大约每个符号 6 比特。压缩后的 Base64 是压缩后二进制文件的 96% 到 107%,是原始文件的 84% 到 109%。这里的 JPEG 本身还能再压缩一点,所以它们的 Base64 版本甚至比 gzip 后的 JPEG 还小一些;这点差异太小,不值得作为依据。极小的 16×16 PNG 上,gzip 固定的 18 字节头部和尾部占比显得很大,而 Brotli 更轻量的封装把这部分消除了。
SVG 反而更糟。SVG 是充满重复的文本,非常适合压缩:漩涡标志用 gzip 从 7,610 字节压到了 3,123 字节。Base64 把每 3 个字节打散成 4 个字符,字符的值取决于对齐位置,所以重复的子串不再呈现相同的样子。gzip 后的 Base64 SVG 比 gzip 后的 SVG 大 41–47%,用 Brotli 则大 48–66%。
URL 编码的 SVG 与 Base64
SVG 本来就是文本,根本不需要 Base64。用 URL 编码的 data URI 可以保持可读:
.flag {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 0v16h2V8h3l1 2h5V2h-4L9 0z'/%3E%3C/svg%3E");
}
至少需要转义 %、#(它会开启 URL 片段)、< 和 >,以及用来包裹属性的那种引号。把 SVG 内部的双引号换成单引号,就不必转义它们了。非 ASCII 字符必须按 UTF-8 做百分号编码。
同样两个 SVG,用同样的方法测量:
| SVG | 编码方式 | 大小 | Gzip | Brotli |
|---|---|---|---|---|
| 图标 | Base64 | 640 | 427 | 410 |
| 图标 | 最小 URL 编码 | 493 | 293 | 253 |
| 图标 | encodeURIComponent | 723 | 326 | 279 |
| 漩涡 | Base64 | 10,148 | 4,398 | 3,823 |
| 漩涡 | 最小 URL 编码 | 7,179 | 3,057 | 2,549 |
| 漩涡 | encodeURIComponent | 11,594 | 3,319 | 2,750 |
“最小”是指合并空白、把双引号换成单引号,只转义 %、#、<、>。它压缩后的大小与原始 SVG 相当;漩涡标志甚至还略小一些,因为合并空白去掉了一些字节。完整的 encodeURIComponent 未压缩时更大,但压缩效果仍远好于 Base64。SVG 转 data URI 工具会把两种形式并排生成出来。
当某个工具或 API 只接受 Base64,或者 SVG 中有你不想费心转义的字符时,SVG 用 Base64 仍然是合理的。
内容安全策略与 data:
在内容安全策略(CSP)下,'self' 不包含 data: URL。每个需要允许它的指令都必须显式列出这个协议:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; font-src 'self' data:
在 img-src 和 font-src 中允许 data: 很常见,风险也低。在 script-src、default-src 或 object-src 中允许它就是另一回事了:能注入标记的攻击者无需任何外部主机就能提供脚本,CSP 的意义也就大打折扣。如果内联图片触发了你的策略,只放宽 img-src 即可。
浏览器还会阻止页面把顶层窗口导航到 data: URL,无论是通过链接、重定向还是脚本。所以 data URI 可以嵌入页面,但不能作为顶层链接打开。
什么时候值得内联
通常值得:
- 极小的图标和界面图片,大约 1–2 KB 以下,尤其是首次绘制就需要的。请求开销在它们的体积中占比很大,而压缩会抹掉大部分 Base64 的代价。
- 单文件 HTML。离线报告、导出的文档、测试覆盖率页面,以及必须能从本地磁盘直接打开的 HTML 邮件预览。
- 关键 CSS,它本来就内联在 HTML 中,小背景图可以跟着一起走。
- 生成的内容,例如脚本把图表渲染成 PNG 后嵌入 HTML 报告。
通常不值得:
- 照片以及任何超过几 KB 的内容。它们更适合作为独立文件,设置较长的缓存时间。
- 在很多页面中共用的图片,内联后每个 HTML 响应都要重新下载一遍。
- Web 字体。一个 WOFF2 文件通常有几十 KB(上面测量的那个是 52 KB),内联到 CSS 中会让样式表阻塞渲染的时间更长,而独立的字体文件可以被缓存和预加载。
- 首屏以下的任何内容。带
loading="lazy"的独立文件在真正需要之前不会产生任何开销。
电子邮件要单独考虑。各邮件客户端对 data: 图片的支持程度不一,请在收件人实际使用的客户端中测试。在 HTML 邮件中嵌入图片的可靠方式是用 cid: 引用的内联 MIME 附件,它同样是 Base64,只不过位于 MIME 结构之中。见 电子邮件中的 Base64。
拖入 PNG、JPEG、WebP 或 SVG,即可得到 data URI 及其大小。文件始终留在你的浏览器中。
常见问题
Base64 图片比普通图片加载更慢吗?
本身不会。小的内联图片可以省掉一次请求;大的则会让 HTML 或 CSS 变臃肿,无法单独缓存,还可能推迟渲染。几 KB 以下内联有好处,超过这个大小就得不偿失。
gzip 能消除 Base64 的 33% 开销吗?
对 PNG、JPEG 和 WOFF2 几乎能完全消除:在我们的测量中,gzip 后的 Base64 与二进制文件相差不过几个百分点。对 SVG 和其他文本则不行,gzip 后的 Base64 SVG 比 gzip 后的 SVG 大 41–47%。
内联 SVG 用 URL 编码好还是 Base64 好?
用 URL 编码。它可读、可编辑,压缩效果和原始 SVG 差不多。至少要转义 #、%、< 和 >;如果属性用双引号包裹,SVG 内部就改用单引号。
为什么我的 data URI 图片被内容安全策略(CSP)拦截了?
CSP 把 data: 当作独立的协议,'self' 不包含它。请在 img-src(字体则是 font-src)中加上 data:,不要把它加到 script-src 或 default-src 中。