上传文件
  1. base64hub
  2. 学习 Base64
  3. 体积开销
参考

Base64 体积开销:精确计算公式

每 3 个字节变成 4 个字符,所以 Base64 比输入大三分之一,再加上最多 2 个填充字符和所有的换行符。

计算公式

对于 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)76CRLF(2 个字节)36.8%
GNU base64、Python encodebytes76LF(1 个字节)35.1%
PEM 证书和密钥64LF35.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 字节426300%
2 字节436100%
3 字节44633.3%
10 字节16141860%
57 字节76767833.3%
100 字节13613414036%
1,000 字节1,3361,3341,37233.6%
1 KiB(1,024)1,3681,3661,40433.6%
1 MB(10⁶)1,333,3361,333,3341,368,42433.3%
1 MiB(1,048,576)1,398,1041,398,1021,434,89833.3%
10 MiB13,981,01613,981,01414,348,93833.3%
25 MB(25 × 10⁶)33,333,33633,333,33434,210,53033.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
仅 Base6446,868
先 Base64,再 gzip17,465
先 gzip,再 Base6416,124
仅 gzip12,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 的测量数据。

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 编码后的大小

输入字节数或粘贴字符串,查看编码前后的大小,可选择是否计入换行。

大小计算器

常见问题

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 等更紧凑的文本编码虽然存在,但支持范围没那么广。

继续阅读