去年接手一个运营后台,列表页打开要九秒多。抓包看下去,接口本身只跑了 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 字节数 | 编码结果 | 说明 |
|---|---|---|---|
| Man | 3 | TWFu | 刚好一组,无填充 |
| hi | 2 | aGk= | 补 1 个 = |
| hello | 5 | aGVsbG8= | 5 不是 3 的倍数 |
| 张三 | 6 | 5byg5LiJ | 中文字约 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 的图。
四、我现在实际的用法
- 接口调试要还原一段可疑字符串时,我直接在浏览器里做,图省事也少一层顾虑——文本、图片、文件都在Base64 编解码里跑,本地完成、不外传;解码中文时不用自己处理编码,页面对 UTF-8 是认得的。
- 要嵌 CSS 的小图标就在同一页把图片转成带
data:image/...;base64,前缀的结果,直接粘进样式表;心里再拿原文件大小乘 4/3 估一遍,决定要不要真的内联。 - 音频、PDF 这类任意文件转字符串,用二进制转 Base64,它会同时列出原始大小和编码后大小,超大文件受浏览器内存限制会失败,这时该走分片上传而不是硬编。
- 要拼进 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 基本就不会错了。