# 什么时候该把图片塞进 CSS，什么时候不该

写进样式表里的图片，跟样式表一起到：不用再发一次请求，也不用等。代价是它不再是一个文件，没法单独缓存，而且它周围的东西一改，它就得重新下一遍。这一页讲这笔交易什么时候划算，什么时候悄悄地不划算。

[打开图片转 Data URI](https://abox.tools/zh/image-to-data-uri/): 整张图变成一行文本，直接粘进 CSS 或者 HTML。

最后更新 2026年8月26日

## 先说结论

打开[图片转 Data URI](https://abox.tools/zh/image-to-data-uri/)，把图丢进去，选*一个 CSS 自定义属性*，把那一行粘到样式表开头。之后哪儿要用，就写 `background-image: var(--logo)`。

图小的时候这么干，图标、项目符号、小箭头、底纹这类，而且每个页面都要用。照片就别这么干。下面讲的，是这两句话为什么不一样，以及怎么判断你手上是哪一种。

## data URI 到底是什么

一个把东西装在自己身体里、而不是指向它的地址。样式表本来会写

```
background-image: url("logo.png");
```

于是浏览器跑一趟去取 `logo.png`；而 data URI 写的是

```
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...");
```

这时候没什么可取的，图已经在那儿了，以字符的形式写出来。它由三部分组成。`data:` 是协议。`image/png` 是媒体类型，而浏览器对它是完全采信的，这一点下面还会说。逗号后面那一整串，就是文件本身。

整个想法就这么点东西。它不是什么奇技淫巧，1998 年就进了标准，之后出的每个浏览器都支持。

## 它换来的：少一次往返

省下的不是流量，是那次请求。

浏览器得先读到提到 `logo.png` 的那份样式表，才能去要它；而它得先把样式表取回来，才能读。所以一张普通的背景图，在页面加载里至少埋着两次往返，而在慢网络上的手机里，一次往返可能就是两百毫秒上下，跟文件多小毫无关系。一个 600 字节的小箭头，传输几乎不花钱，到达却可能要花掉四分之一秒。

内联之后，它跟着样式表一起到。好处全部就在这儿，而对一个出现在首屏的小图标来说，这好处是实打实的。

## 它的代价：三分之一，外加缓存那笔账

**base64 多出大约三分之一。**三个字节的文件变成四个字符，因为用网址允许的那些字符去表示任意字节，代价就是这么多。没有哪个聪明的编码器能绕开它。一张 9 KB 的 PNG，进样式表就是 12 KB。

**压缩还不回来。**这一节正是大家想当然略过的。gzip 和 Brotli 靠的是找冗余，而 PNG、JPEG 和 WebP 早就压过一轮了，里头几乎没有冗余可找，base64 也不会添出冗余来。实际上你只能找回多出来那三分之一里的十分之一左右，绝不是全部。（SVG 正好相反，下一节专门说它。）

**它不再是一个文件。**这笔代价不会出现在你多半会做的任何一项测量里，而规模一大，要命的就是它：

- **没法单独缓存。**普通图片取一次，接下来一年都在复用。内联的那张是样式表的一部分，样式表的缓存条目活多久，它就活多久。
- **改动任何东西，全都得重下。**改一个边距，发一份新样式表，于是每个访客连着那张两年没变过的图，一起再下一遍。
- **它压在关键路径上。**样式表会阻塞渲染，图片不会。把图片内联进去，等于把它从第二类挪进第一类：整份东西，连图带样式，全到齐之前，页面画不出来。
- **没法并行下载。**浏览器一次能下很多东西。内联的图不是独立的一样东西，所以这份好处它一点都沾不上。

几个粗略的分界线，它们是建议改变的地方，不是浏览器行为改变的地方：2 KB 以内是明摆着的赚；到 10 KB 上下，只要它每页都要用，一般还是值；过了 50 KB，那就是个错，而且没有任何报错提醒你。[这个工具](https://abox.tools/zh/image-to-data-uri/)会告诉你每个结果落在哪一档，字符数就摆在旁边。

![输出卡片：一条含 base64 数据 URI 的 CSS 规则，旁边是原文件大小和编码后的大小。](https://abox.tools/screens/embed-an-image-in-css/output.webp)

编码后的副本比来源文件大三分之一左右。这一节要讲的代价就是它，而且是写出来的，不是留给你自己去发现的。

## 绝不要给 SVG 上 base64

这是内联图片里最常见的一个错，而且导出插件和构建工具犯得不比人少。

SVG 是文本。网址本来就能装文本。只有少数几个字符必须转义，`%`、`#`、`<`、`>`，还有你用来包住它的那个引号，其余的原样留着就行。这么编出来的 URI，通常比同一个文件的 base64 短五分之一左右，而且之后是按文本压缩的，不是按噪声。

它还看得懂：

```
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E...");
```

`viewBox` 你看得见。填充色也可以直接在编辑器里改，不用解码任何东西。同一个文件上了 base64，就变成一堵谁也不会再碰的字母墙。[图片转 Data URI](https://abox.tools/zh/image-to-data-uri/) 对认出来是 SVG 的文件自动走这条路，另外给个人别工具链留了个勾选框，那种非 `;base64` 不可的情况才用。

## 那个只坑 SVG 的引号问题

CSS 允许你写不带引号的 `url()`，对普通文件名来说这没问题：

```
background-image: url(logo.png);
```

同样的写法用在百分号编码的 SVG 上就完了。不带引号的 `url()` 记号，遇到第一个空格、括号、引号或者控制字符就结束，而 SVG 里到处是空格，每个属性之间、路径里每两个数字之间都有。于是这条声明变成无效声明，而 CSS 对无效声明是一声不吭地丢掉，你得到的是没有背景，也没有报错。

办法就是加引号，每次都加：

```
background-image: url("data:image/svg+xml,%3Csvg ... %3E");
```

这也正是编码器不需要转义空格的原因：引号包着的网址里，空格完全合法，而把每个空格都写成 `%20`，文件里有多少空格就得多掏多少个三倍字符。这两个决定是一起的：URI 加引号，空格就能原样留着。这个工具输出的每一种形式都加了引号，就是为了这个。

## 媒体类型必须写对

data URI 自己声明自己的类型，而浏览器完全采信这句话。这里没有取文件时那种嗅探兜底：一个其实是 JPEG 的东西，你声明成 `image/png`，图就是画不出来，而且哪儿都不会给你一句有用的提示。

这一点要紧，因为扩展名会撒谎。一张按 JPEG 导出、又被改名成 `logo.png` 的照片，在硬盘上是再平常不过的东西。而图片文件开头那几个字节说得毫不含糊，每种格式都有自己的签名，所以工具该读文件，不该读名字。这里的这个就是这么干的，两边对不上时还会告诉你一声。

有两种格式值得单说，因为它们失败得很让人摸不着头脑。**HEIC**，iPhone 拍照用的那个，还有 **TIFF**，扫描仪出的那个，都能生成完全合法的 data URI，可除了 Safari 没有浏览器画得出来。URI 没坏，只是网页本来就不支持这个格式。先转一道再说。

## 你没打算公开的那些元数据

data URI 是文件的逐字节副本。没有哪一步解码再重编，这通常正是重点，画质一点不掉，但同时也意味着文件里别的一切都跟着来了。

一张刚出手机的照片带着 EXIF：拍摄地的 GPS 坐标、时间戳、相机型号，常常还有它的序列号。这些能占到 30 KB。内联之后就是你样式表里 40 KB 的 base64，压在每个页面的关键路径上，同时还把一个家庭住址以谁也想不到去看的形式提交进了仓库。

先用 [EXIF 查看与清除](https://abox.tools/zh/exif-editor/)清掉它，那个工具只重写容器，画面碰都不碰，[另有一篇指南](https://abox.tools/zh/guides/remove-exif-and-gps-data/)讲它。图片转 Data URI 会读出 JPEG、PNG 或 WebP 里到底带了多少元数据，在你复制任何东西之前就告诉你。

## 拿到之后，该放哪儿

图只在一条规则里出现，那就把 URI 写在那条规则里。要是出现在不止一条里，而图标通常都不止，把悬停状态和深色主题一算就明白，那就声明成一个自定义属性，只声明一次：

```
:root {
  --icon-search: url("data:image/svg+xml,%3Csvg ... %3E");
}

.search-field { background-image: var(--icon-search); }
.search-button::before { content: var(--icon-search); }
```

一个 3 KB 的 URI 粘进四条规则，就是 12 KB 的样式表，外加图标一改要动四个地方。自定义属性两样都只有一份。而且这也是让主题切换成立的写法：在媒体查询里重新定义 `--icon-search`，所有用到它的地方跟着变。

要是用在 `<img>` 标签而不是 CSS 里，记得写上 `width` 和 `height`。内联图片是瞬间加载的，所以缺尺寸造成的布局抖动快到你看不见，却照样记在你账上。SVG 是例外：一个只带 `viewBox` 的 SVG 本来就没有自己的像素尺寸，把浏览器那个 ⁦300×150⁩ 的默认值写到标签上，等于给一张能任意缩放的图钉了一个谁也没选过的尺寸。

`alt` 留空，除非你真有一句实话可写。这张图到底有实际含义还是纯装饰，只有你知道；而拿文件名瞎猜一句描述，对用读屏软件的人来说，比没有描述更糟。

![形态卡片：几个按钮决定输出什么，CSS 背景规则、img 标签或裸的 URI，另有一个 base64 与原样 SVG 的开关。](https://abox.tools/screens/embed-an-image-in-css/shape.webp)

去处决定产出，所以先问清楚，而不是丢给你当成复制粘贴的练习。

## 什么时候答案是“别内联”

图片编码之后超过 50 KB 上下，内联就是选错了工具，编码上再怎么讲究也补不回来。替代办法，按值得一试的顺序：

- **把它做小。**大到不适合内联的图片，多半本来就太大了。[图片压缩](https://abox.tools/zh/compress-image/)能把一张照片压到你指定的大小，[图片缩放](https://abox.tools/zh/resize-image/)能把像素尺寸砍到版式实际用得上的那么大，而后者往往才是真正的问题所在。
- **改画成 SVG。**一个导出成 40 KB PNG 的图标，常常就是一个 900 字节的 SVG。这不是压缩率的差别，是格式的差别，而且顺手把高分屏那件事也解决了。
- **让它继续当文件，然后预加载。**`<link rel="preload" as="image">` 会立刻开始取它，同时不把这些字节挪到关键路径上。内联的好处它占了大半，缓存那笔代价它一分不付。

## 这些事一样都不用上传

把一个文件编成 base64 就是做算术。用的是浏览器从一开始就带着的两个函数，`btoa` 和 `encodeURIComponent`，而一张图为了换个写法就跑一趟服务器再回来，技术上毫无理由。凡是要你上传文件才肯干这件事的转换器，都是为它自己的理由在上传，不是为你的。

[这里的工具](https://abox.tools/zh/image-to-data-uri/)哪儿都不发：页面的 `Content-Security-Policy` 把它可能联的地址全列了出来，没有一个是本站的。与其听我们说，不如把页面打开，拔掉网线，照编一张试试。[把文件传给在线转换器安全吗？](https://abox.tools/zh/guides/is-it-safe-to-upload-files/)里还有另外三个检查，用在任何工具上都管用。
