计算公式
对于 n 个字节的输入,带填充的 Base64(RFC 4648 的默认方式)长度恰好是:
length = 4 × ceil(n / 3)
不带填充时(例如 JWT 中的 Base64URL),长度是:
length = ceil(4n / 3)
两者相差的就是 = 的个数,根据 n mod 3 的不同,为 0、1 或 2。填充指南 解释了它们的来由。
反过来,长度为 L 的带填充 Base64 字符串,解码后的字节数为:
bytes = 3 × L / 4 − (number of trailing "=")
所以 TWFu 是 3 个字节,TWE= 是 2 个,TQ== 是 1 个。计数之前要先去掉空白和换行符。Base64 大小计算器 支持两个方向的计算,包括换行符和 Data URI 前缀。
用代码表示:
import base64, math, os
for n in [1, 2, 3, 100, 1000]:
data = os.urandom(n)
assert len(base64.b64encode(data)) == 4 * math.ceil(n / 3)
const b64Len = (n) => 4 * Math.ceil(n / 3);
const rawLen = (s) => (s.length * 3) / 4 - (s.endsWith("==") ? 2 : s.endsWith("=") ? 1 : 0);
b64Len(1000); // 1336
rawLen("TWE="); // 2
为什么开销是 33%
一个字节有 8 位。一个 Base64 字符承载 6 位数据,因为它是 64 = 2⁶ 个符号之一,但它是以 8 位的 ASCII 字节存储的。所以每个输出字节只有 6 位有用,输出必须是输入的 8/6 = 4/3。也就是多出 33.3%,而且这是一个硬下限:任何 Base64 实现都无法做得更好,除非放弃这套字母表。(比特如何重新分组,见 Base64 的工作原理 中的逐步演示。)
对于很小的输入,填充会让比例更糟,因为输出总是以 4 个字符为一块。一个字节会变成 4 个字符,增加了 300%。到几百个字节时,填充的影响就可以忽略不计,比例稳定在 4/3。
更紧凑的文本编码也是有的。Base85 用 5 个字符存储 4 个字节(开销 25%),Base91 还要略低一些。不过它们的支持范围没那么广,字母表中还包含在某些场景下会惹麻烦的字符。对比见 Base64 与 hex、Base32、Base58、Base85 的对比。
换行:MIME、PEM 与命令行工具
有些格式会把 Base64 分成多行,而行尾字符也计入大小。
| 格式 | 每行长度 | 行尾 | 总开销 |
|---|---|---|---|
| 纯 Base64(RFC 4648) | 不换行 | 无 | 33.3% |
| MIME 电子邮件(RFC 2045) | 76 | CRLF(2 个字节) | 36.8% |
GNU base64、Python encodebytes | 76 | LF(1 个字节) | 35.1% |
| PEM 证书和密钥 | 64 | LF | 35.4% |
MIME 的数字来自 4/3 × 78/76:每 76 个 Base64 字符会多出 2 个字节的 CRLF。设 L = 4 × ceil(n / 3),在每一行(包括最后一行)之后都加 CRLF 时,大小为:
mime_length = L + 2 × ceil(L / 76)
每个完整的 76 字符行恰好容纳 57 个输入字节,这就是 MIME 编码器以 57 字节为单位分块处理数据的原因。电子邮件的更多内容见 电子邮件中的 Base64,MIME Base64 编码器 可以生成分行的输出。
大小对照表
用上面的公式计算,并与 Python 的 base64 模块进行了核对。
| 输入 | 带填充的 Base64 | 不带填充 | MIME(76 + CRLF) | 开销(带填充) |
|---|---|---|---|---|
| 1 字节 | 4 | 2 | 6 | 300% |
| 2 字节 | 4 | 3 | 6 | 100% |
| 3 字节 | 4 | 4 | 6 | 33.3% |
| 10 字节 | 16 | 14 | 18 | 60% |
| 57 字节 | 76 | 76 | 78 | 33.3% |
| 100 字节 | 136 | 134 | 140 | 36% |
| 1,000 字节 | 1,336 | 1,334 | 1,372 | 33.6% |
| 1 KiB(1,024) | 1,368 | 1,366 | 1,404 | 33.6% |
| 1 MB(10⁶) | 1,333,336 | 1,333,334 | 1,368,424 | 33.3% |
| 1 MiB(1,048,576) | 1,398,104 | 1,398,102 | 1,434,898 | 33.3% |
| 10 MiB | 13,981,016 | 13,981,014 | 14,348,938 | 33.3% |
| 25 MB(25 × 10⁶) | 33,333,336 | 33,333,334 | 34,210,530 | 33.3% |
最后一行对应的是电子邮件的情况。SMTP 的大小限制(SIZE 扩展,RFC 1870)按传输时的邮件计算,包括编码在内,所以一个 25 MB 的文件在传输时会变成约 34.2 MB,无法通过 25 MB 的服务器限制。有些邮件服务商描述的限制是针对附件本身的,所以请确认你的服务商是怎么计算的。
压缩的影响
压缩与 Base64 的相互作用有两种方式,而且顺序很重要。
只要两端都由你控制,就先压缩再编码。压缩后数据的 Base64,是一个更小数字的 4/3。我们用 Node.js zlib 在 gzip 9 级下对 GPL-3 许可证文本(35,149 字节)做了测量:
| 处理流程 | 大小 |
|---|---|
| 原始文本 | 35,149 |
| 仅 Base64 | 46,868 |
| 先 Base64,再 gzip | 17,465 |
| 先 gzip,再 Base64 | 16,124 |
| 仅 gzip | 12,091 |
Gzip + Base64 工具可以在浏览器中完成“先 gzip,再 Base64”这一流程。
对于二进制数据,传输层压缩能收回大部分开销。如果一个 Base64 字符串放在经过 gzip 或 Brotli 压缩的 HTTP 响应中传输,压缩器会把每个 6 位的字符重新压回接近 6 位。对于 1 MiB 的随机字节,Base64 后增长到 1,398,104 个字符;gzip 后为 1,053,023 字节(比原始数据多 0.4%),Brotli 则把它降到 1,048,805。同样的数据带上 MIME 换行后,gzip 结果为 1,088,689,所以即使经过压缩,CRLF 也要多占约 3.4%。
文本则不同。Base64 打乱了压缩器所依赖的重复模式,所以 Base64 文本的压缩效果比原文本差得多:上表中是 17,465 字节对 12,091 字节。Data URI 的性能 一文给出了 PNG、JPEG、WOFF2 和 SVG 的测量数据。
URL、请求头和 Cookie 的长度限制
Base64 会让你放进 URL、请求头或 Cookie 中的任何内容变大,而这三者都有长度限制。
- URL:RFC 9110 建议发送方和接收方至少支持 8,000 个八位字节的 URI。许多服务器的默认限制就在这附近:nginx 为请求行分配 8 KB 的头部缓冲区,Apache 的
LimitRequestLine默认为 8,190 字节。更长的 URL 通常会以414 URI Too Long失败。用 Base64URL 的话,大约能容纳 6,000 字节的数据,还要减去 URL 的其余部分。 - 请求头:同样的服务器缓冲区也作用于每一行请求头,这对
Authorization: Bearer ...头中的长 JWT 以及较大的 Basic Auth 凭据很重要。 - Cookie:RFC 6265 要求浏览器为每个 Cookie 至少支持 4,096 字节,包括名称和属性在内。经过 Base64 编码后,大约能放 3 KB 的数据。
如果 Base64 载荷接近这些限制中的任何一个,请改为放在请求体中发送,或者在服务器端存储,只传一个 ID。
输入字节数或粘贴字符串,查看编码前后的大小,可选择是否计入换行。
常见问题
Base64 编码后比原文件大多少?
只要输入超过几个字节,就大约多 33.3%。带 MIME 换行(76 个字符加 CRLF)时约为 36.8%。
如何根据 Base64 字符串算出原始大小?
取去掉空白后的长度,乘以 3/4,再减去末尾 = 的个数。例如 TWE= 是 4 × 3/4 − 1 = 2 个字节。
为什么 1 个字节的输入会生成 4 个字符?
使用填充时,Base64 总是输出完整的 4 字符块。一个字节填满 2 个字符,另外 2 个是 =,所以很小的输入相对开销要高得多。
能让 Base64 变小吗?
在编码之前先压缩数据,例如用 gzip;如果接收方允许,再去掉填充。Base64 本身的比例是固定的;Base85 等更紧凑的文本编码虽然存在,但支持范围没那么广。