杂记 › 技术笔记 › 图片转Base64反而更慢?Base64与Data URI内嵌边界

图片转Base64反而更慢?Base64与Data URI内嵌边界

去年接手一个运营后台,列表页打开要九秒多。抓包看下去,接口本身只跑了 300 毫秒,返回体却有 22MB——每条记录都把商品主图整个塞进了 JSON,字段长这样:"pic": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."。单张原图 850KB,编码完 1.13MB,一页 20 条就爆到这个量级。当时写这段代码的人理由很正当:"省一次图片请求"。要理解为什么这个理由只在很小的图上成立,得先把 Base64 这件事讲清楚。

一、它解决的是"搬运",从来不是"保护"

Base64 干的活很窄:把任意字节流换成只用 64 个可打印字符写成的文本,让它在只认文本的通道里安全通过——JSON、XML、邮件头、URL 查询串都算这类通道。早期邮件协议只能传 7 位 ASCII,附件里的二进制必须换个说法;JSON 字段也一样,塞进原始字节会直接把解析器打断。

但"换个说法"不等于加密。它没有密钥,没有口令,任何人都能把结果原样还原回去。我见过真实的例子:有项目把用户手机号 Base64 之后写进日志,安全评审时被一句话问死——这跟没处理有什么区别。JWT 也是同一个道理,它的 Header 和 Payload 只是 Base64Url 编码,把字符串粘进控制台就能读出里面的手机号、角色和到期时间;真正的安全来自第三段签名,不是那层编码。

二、为什么会变大,末尾的 = 是怎么来的

编码表是 A-Z、a-z、0-9、+、/。规则是把 3 个字节(24 位)切成 4 段、每段 6 位,每段落进 0 到 63 的索引取一个字符。凑不满 3 字节的部分用 = 补齐,所以结尾常出现一到两个等号。代价写死在数学里:3 字节换 4 字符,体积变成原来的 4/3。

原文UTF-8 字节数编码结果说明
Man3TWFu刚好一组,无填充
hi2aGk=补 1 个 =
hello5aGVsbG8=5 不是 3 的倍数
张三65byg5LiJ中文字约 3 字节,正好两组

用这张表能顺手解释两个日常疑问:为什么中文 Base64 后长度总是 4 的倍数(UTF-8 下一个汉字通常 3 字节),以及为什么看到 = 结尾不代表出错,只是补位。

三、图片内联到底什么时候划算

Data URI 的写法是 data:[MIME类型];base64,[内容],可以直接进 <img src>,也能当 CSS 背景。它省掉的是一次 TCP 连接和请求往返,代价是这块内容跟着 HTML 一起走,无法被浏览器单独缓存——首页 HTML 一改,内联图片就得整个重传。

我的经验值卡在 2KB:以内的小图标、箭头、占位图形内联,收益明确;再大就换成独立文件,并且给它加指纹和长缓存。上面那次故障里的 850KB 商品图,编码后 1.13MB,gzip 也救不回来——原始二进制里的冗余已经被 6 位分组打散,压缩率明显低于直接压原文件。同类需求我会先把大图留在 CDN 上,只把 16×16 的图标内联,那部分体积膨胀三成就完全可以接受。

不适合内联的还有:任何用户可上传的图、需要被搜索或复用的图、以及需要单独设 Cache-Control 的图。

四、我现在实际的用法

  1. 接口调试要还原一段可疑字符串时,我直接在浏览器里做,图省事也少一层顾虑——文本、图片、文件都在Base64 编解码里跑,本地完成、不外传;解码中文时不用自己处理编码,页面对 UTF-8 是认得的。
  2. 要嵌 CSS 的小图标就在同一页把图片转成带 data:image/...;base64, 前缀的结果,直接粘进样式表;心里再拿原文件大小乘 4/3 估一遍,决定要不要真的内联。
  3. 音频、PDF 这类任意文件转字符串,用二进制转 Base64,它会同时列出原始大小和编码后大小,超大文件受浏览器内存限制会失败,这时该走分片上传而不是硬编。
  4. 要拼进 URL 的参数,先用URL 编解码过一遍再发,别指望 Base64 结果能原样躺在查询串里。

五、踩过才知道的几个坑

  • 加号被吃掉:Base64 字符表里的 + 在查询串里按旧约定表示空格。一段合法编码传过去解出来内容全变,多半就是这个。要么先 urlencode,要么改用把 +、/ 换成 -、_ 且不补 = 的 Base64Url(JWT 用的就是它)。
  • atob 解中文乱码:atob() 返回的是按字节拼出来的字符串,中文会碎成 ö¼¡ 这类东西。要么 decodeURIComponent(escape(atob(s))),要么用 TextDecoder;Node 里 Buffer.from(s,'base64').toString('utf8') 才是对的做法。
  • 换行导致解码失败:从邮件、文档或日志里复制出来的长串经常带 \r\n 或空格,Python 的 b64decode 会直接抛 binascii.Error: Invalid base64-encoded string。先把空白清掉再解。
  • 长度不对齐:Base64 结果长度必是 4 的倍数。如果拿到 43 个字符,说明开头或结尾被截了——这在抓包工具的截断显示里特别常见。
  • MIME 写错:PNG 的前缀写成 data:image/jpeg;base64,,浏览器不一定报错,但会显示成破图;自己手拼时最好整段由工具生成。

六、常见疑问

Q:Base64 能当加密用吗?

不能。它是无密钥的可逆编码,任何拿到密文的人都能还原,需要保密请用真正的加密算法,需要存口令请用带盐的哈希。

Q:为什么我编码完体积大了三分之一?

因为每 3 个字节必须用 4 个字符表达。这是"用可打印字符承载二进制"的固定开销,跟内容是不是图片无关。

Q:小图片内联真的能提速吗?

在两三 KB 的量级上,省掉一次请求往返的收益大于体积膨胀;再往上就是净亏损,改成独立文件加缓存更快。

Q:末尾的一两个 = 要去掉吗?

标准 Base64 里它们是补位符,解码时留着最稳。有些语言允许严格模式校验,去掉反而会报长度错误。

Q:能不能用 Base64 传大文件?

不建议。体积涨三成、要整块读进内存、也断了流式处理的可能,几 MB 就该走对象存储或分片上传。

回头看那个九秒的列表页,改动其实只有一行:把图片字段换成 URL,再给图标留内联。判断标准也简单了——问这段内容是要"搬过去"还是要"藏起来",问它是不是小于 2KB,两个问题答完,用不用 Base64 基本就不会错了。

📎 相关文章

PNG 压了三遍只减 426 字节:无损压缩的真实边界 2026-10-22 WebP 转 PNG 后体积涨了几倍:什么时候必须转,怎么把涨幅压住 2026-10-22 截图缩到四分之一,小字糊成一团的三种原因 2026-10-22 同样裁 300×300,头像为什么总偏右下角半格 2026-10-22 按每月 1.7 倍预估 18 个月,脚本和表格为什么给出两个数 2026-10-22