编码 · Base64

Base64 不是压缩也不是加密:33% 膨胀和 data URL 的真实代价

见过有人把密码 Base64 一下当"加密"存进库,也见过把首屏所有图标内联成 data URL "省请求",结果首屏更慢了。Base64 是个被严重误解的东西——它不缩小数据,反而撑大约 33%;它不保护数据,谁都能一秒解开。它只解决一个问题:让二进制安全地走过只认文本的通道。这篇说清楚它的代价该花在哪。

Base64 干的事只有一件:把任意二进制数据,重新编码成 64 个"安全"可见字符(A-Z a-z 0-9 + /)。这样它能塞进 JSON 字符串、URL、邮件正文、HTML 这些只接受文本的地方而不被破坏。仅此而已。它不是给人看的,也不是给数据上锁的。

33% 从哪来:4 个字符装 3 个字节

原理是进制重组。原始数据按 8 位一字节,Base64 按 6 位一组(2⁶=64 正好对应 64 个字符)。每 3 字节(24 位)切成 4 组 6 位,编成 4 个字符。

3 字节  →  4 字符      膨胀率 4/3 ≈ 1.333
即每 100KB 的原始数据,Base64 后约 133KB

当原始字节数不是 3 的倍数时,末尾用 = 补齐到 4 的倍数——这就是 Base64 串结尾常见一两个 = 的原因。它是填充,不是数据。

⚠️

别在 Base64 之后再指望 gzip/brotli 帮你省回来。压缩算法吃的是原始字节里的规律,Base64 重组后规律被打散,压缩率反而比直接压原始二进制更差。正确顺序是先压缩、再 Base64(如果非传文本不可),不是反过来。

base64url:URL 和 JWT 里的那个变种

标准 Base64 的 +/= 在 URL 里有特殊含义:+ 会被解析成空格,/ 是路径分隔,= 是查询参数赋值。直接把 Base64 塞进 URL 会被搅乱。于是有了 base64url(RFC 4648 的 URL-safe 变体):

标准:    +  /  =(填充)
URL 安全: -  _  (通常直接省略填充)

JWT 就是典型——它的三段都是 base64url 且不带填充。这也是为什么你把 JWT 中段直接拿标准 Base64 解码偶尔会失败:字符集和填充都不一样。处理 JWT、URL 参数里的 token 时,认准 base64url。要拆开看一个 JWT 三段分别编码了什么,用 JWT 编辑器 比手动解码省事。

data URL 内联图片:HTTP/2 之后大多是亏的

把小图标编成 data:image/png;base64,... 直接写进 HTML/CSS,曾经是性能优化的常规操作——省掉一次 HTTP 请求。在 HTTP/1.1 时代,每个请求都有排队和连接开销,这么换划算。

但到了 HTTP/2,多路复用让同一连接上并发几十个请求几乎没有额外开销,"省一个请求"这点收益基本归零。而 Base64 内联的代价却一个没少,还更明显了:

💡

现在的经验法则:只对极小(约 1–2KB 以内)、且几乎每页都要、又不常变的资源考虑内联,比如一两个关键 SVG 图标。稍大的图老老实实走外链,交给 CDN 和浏览器缓存。别再无脑"内联省请求"了——HTTP/2 之后那笔账已经反过来了。

btoa 遇到中文直接抛错

浏览器自带 btoa/atob,但它们只认 Latin1(每字符 0–255)。传个中文或 emoji 进去,直接报错:

btoa("hello")     // "aGVsbG8="  ✓
btoa("你好")       // ❌ InvalidCharacterError: 字符超出 Latin1 范围

正确做法是先用 TextEncoder 把字符串编成 UTF-8 字节,再对字节做 Base64:

// 编码:字符串 → UTF-8 字节 → Base64
function b64encode(str) {
  const bytes = new TextEncoder().encode(str);
  let bin = "";
  bytes.forEach(b => bin += String.fromCharCode(b));
  return btoa(bin);
}
// 解码反过来:Base64 → 字节 → 字符串
function b64decode(b64) {
  const bin = atob(b64);
  const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}
b64encode("你好")            // "5L2g5aW9"
b64decode("5L2g5aW9")        // "你好"

这个坑的根源还是字符和字节的混淆——btoa 要的是字节,你给了它字符。Node 里则用 Buffer.from(str, "utf8").toString("base64"),没这个问题。

最后纠正那个最危险的误解

Base64 不提供任何安全性。它是公开、可逆、无密钥的编码,任何人拿到都能一行命令还原:

echo "cGFzc3dvcmQxMjM=" | base64 -d
# password123

把密码、token、敏感配置 Base64 一下就当"处理过了",等于没处理。要保密用加密(AES 等),要校验完整性用哈希/HMAC。Base64 只负责"能安全地放进文本通道",安全性这词跟它没关系。需要快速编解码一段文本或文件,用 在线 Base64 工具,本地计算不上传。

在线 Base64 编解码

文本与文件的 Base64 编码、解码,支持 URL-safe 变体,全本地计算

打开 Base64 工具