DEVTOOLBOX / BENCHMARKS
实测数据:哈希、Base64、gzip、JSON、PDF 与图片编码
下面每个数字都由本仓库的脚本在同一台机器上跑出,机器信息放在最前面。重点不是绝对值,而是分清哪些代价是结构性的(Base64 恒定膨胀三分之一),哪些是硬件相关的(这里 SHA-256 反而比 MD5 快)。
在哪里、用什么方法测的
所有耗时都取 3–5 次运行的中位数,且在空闲机器上完成,每一行都来自同一台机器。这里没有任何数字是从别处抄来或估算的:脚本自己构造输入(随机缓冲区、合成记录数组、现场生成的 PDF、canvas 渲染的图片),报告它实际观察到的时间。
这件事很重要,因为网上流传的基准数字通常不可比。一个没有写明算法、数据量、运行时版本与硬件的吞吐数字,不是测量结果,只是轶事。你在自己机器上重跑这个脚本,比值应该接近,绝对速度会不同——有时差一个数量级。
| 项目 | 值 |
|---|---|
| CPU | Apple M5 |
| 核心数 / 内存 | 10 / 32 GB |
| 操作系统 | macOS 27.0.1 |
| 运行时 | Node v20.20.2 |
| 浏览器(图片实测) | Chromium 151.0.7922.34 |
| 数据生成日期 | 2026-10-22 |
哈希吞吐:这里 SHA-256 比 MD5 快
这是全页最反直觉的一行。在这台机器上,SHA-256 处理同一块 64 MB 缓冲区大约比 MD5 快 3.7 倍,SHA-1 与 SHA-256 基本持平。原因不是 SHA-256「更轻」,而是现代 ARM 芯片带有专门的 SHA-1/SHA-256 指令:被硬件加速的算法几乎免费,而没有硬件路径的算法只能靠通用指令硬算。
由此得出两个实用结论。第一,不要因为「MD5 更快」就选 MD5,在当前硬件上这个前提常常是错的,要用实测说话。第二,本来也不该用 MD5——理由是抗碰撞性已经失守,与速度无关。SHA-512 在这里居中,是因为它按 64 位字处理:在 64 位 CPU 上每字节参与的字更少,但没有宽位硬件支持时达不到 SHA-256 的速率。
| 算法 | 数据量 | 耗时 | 吞吐 |
|---|---|---|---|
| MD5 | 64 MB | 82 ms | 778 MB/s |
| SHA1 | 64 MB | 22 ms | 2,922 MB/s |
| SHA256 | 64 MB | 22 ms | 2,910 MB/s |
| SHA512 | 64 MB | 39 ms | 1,648 MB/s |
吞吐是硬件与运行时的属性,不是算法名字的属性。用速度做理由之前先实测。
Base64:4/3 这条规则是精确的,可以提前算出来
每一行都落在 1.3333(只有极小的输入受填充影响而略高:1024 字节得到 1368 个字符,比值 1.3359)。公式是 ceil(bytes / 3) × 4,也就是说不用编码就能预估长度。10 MB 文件会变成 13.98 MB;一张 5 MB 的截图内嵌到 CSS 会成为 6.67 MB 样式表,每次加载都要解析。
填充那一列是实操陷阱:只要原长度不是 3 的倍数,就会出现一到两个 =,而严格解码器会拒绝长度不是 4 的倍数的载荷。所以拼接两段 Base64、或把它按行折行,都会破坏这个不变量——带换行的载荷在解码前必须去掉空白。
| 原始大小 | 编码后 | 膨胀比 | 填充 |
|---|---|---|---|
| 1.0 KB | 1.3 KB | 1.3359 | 2 × "=" |
| 64.0 KB | 85.3 KB | 1.3334 | 2 × "=" |
| 1.00 MB | 1.33 MB | 1.3333 | 2 × "=" |
| 10.00 MB | 13.33 MB | 1.3333 | 2 × "=" |
编码后长度 = ceil(字节数 / 3) × 4。放不进预算时,答案是压缩或改走二进制通道,而不是「用小一点的 Base64」。
gzip:JSON 压到七分之一,随机字节一点也压不动
结构化、重复性高的数据压缩收益极大:302 KB 的 JSON 数组压到 41 KB,比值 0.137;重复文本收益更大。而随机字节基本停在 1.0 附近——gzip 找不到不存在的冗余,只剩几字节的帧开销。
发布接口或可下载资源前值得养成这个检查习惯:JSON、HTML、CSS、SVG、日志几乎总是值得压缩;已压缩格式(PNG、JPEG、ZIP、gzip 自身)再压只烧 CPU 没有收益。这也解释了为什么 Base64 放在经过 gzip 的 HTTP 响应里,比落盘保存要「没那么浪费」:体积膨胀依然存在,但编码后的文本本身可被压缩。
| 样本 | 原始大小 | gzip 后 | 压缩比 |
|---|---|---|---|
| JSON array (5000 records) | 295.1 KB | 40.4 KB | 13.7% |
| Repeated text | 87.9 KB | 342 B | 0.4% |
| Random bytes | 256.0 KB | 256.1 KB | 100.0% |
只在正确的层压缩一次。重复压缩已压缩格式,只是浪费 CPU。
JSON 规模:十万条记录解析是几十毫秒的量级
解析 10.4 MB 的 JSON 约 33 ms,把同样的结构转回字符串约 17 ms——快到「CPU 太慢」很少是大 JSON 把标签页搞崩的原因。真正致命的是内存与主线程:解析出的对象图占用文本体积的数倍,而一次同步解析会在整个过程中阻塞渲染。
这正是 JSON Lines 存在的理由。把载荷拆成一行一个对象,每行都是可以独立解析、用完即弃、出错可续的单元——用略大的总字节数换取恒定内存与非阻塞的流水线。一次性粘贴 10 MB,数组更省事;面对 2 GB 的导出,这就不是选择题了。
| 记录数 | 字节数 | JSON.parse | JSON.stringify |
|---|---|---|---|
| 1,000 | 95.9 KB | 0.3 ms | 0.2 ms |
| 10,000 | 988.0 KB | 2.6 ms | 1.9 ms |
| 100,000 | 9.93 MB | 33.3 ms | 17.2 ms |
解析时间随字节数线性增长,所以要看字节而不是猜上限。真正先撑不住的是内存,不是 CPU。
PDF 合并:复制页面很便宜,页面内容才是体积来源
合并 200 页现场生成的 PDF 约 24 ms,因为这个操作只复制页面对象、从不栅格化:文字仍是文字,图片保持原有编码。这里 200 页产物只有 39 KB,因为输入是合成的——每页只有一行短文本。
真实场景的合并行为值得记住:耗时随页面与对象数量增长,但文件大小由页面里原有的图片决定。两份 50 MB 的扫描件合并同样很快,结果仍接近 100 MB。所以「合并很快」和「合并后很小」是两件不相关的事,只有前者是关于合并本身的。
| 输入 | 合并后页数 | 耗时 | 产物大小 |
|---|---|---|---|
| 2 × 5 页 | 10 | 6 ms | 2.5 KB |
| 5 × 20 页 | 100 | 16 ms | 19.7 KB |
| 10 × 20 页 | 200 | 24 ms | 38.8 KB |
合并保留内容、不重新编码。产物体积大致等于各输入之和。
图片编码:同一张 1600×1200 的图,六种存法
同样的 canvas 内容存成 PNG 是 2.36 MB;JPEG 质量 0.75 是 80 KB;WebP 质量 0.75 是 39.6 KB——相对 PNG 缩小 59 倍。PNG 是无损的,因此把测试图里的每个噪点都保留下来,这也解释了为什么「照片截图」体积爆炸、「纯文字截图」却很小。
由此得到的判断规则:照片与渐变图走 WebP(兼容性要求时用 JPEG),只有带锐利边缘、需要透明度、或文字必须像素级精确的图才留在 PNG。另外注意质量参数不是线性的——WebP 从 0.9 降到 0.75 体积减半,而从 0.75 降到 0.5 只再省四分之一,代价是渐变处肉眼可见的模糊。
| 格式 | 质量 | 文件大小 | 相对 PNG |
|---|---|---|---|
| PNG | 无损 | 2.25 MB | 1.000× |
| JPEG | 0.92 | 159.2 KB | 0.069× |
| JPEG | 0.75 | 78.3 KB | 0.034× |
| WebP | 0.90 | 75.8 KB | 0.033× |
| WebP | 0.75 | 38.7 KB | 0.017× |
| WebP | 0.50 | 29.6 KB | 0.013× |
选格式让体积差一个数量级;调质量只能在两倍以内挪动。
这些数字不能告诉你的事
- 它们只描述一台机器、一个运行时版本、每种一个输入形状。换硬件绝对值会变,但结构性比值(Base64 的 4/3、JSON 的 gzip 收益、PNG 与 WebP 的差距)依然成立。
- 端到端体验还包括传输、解压与渲染。一次 40 ms 的解析如果挡住首屏,仍然会让人觉得慢。
- 浏览器内工具同时受内存与速度约束。2 GB 的文件会在这些耗时变得相关之前就先因内存溢出崩掉。
- 数据可以随时刷新:
node scripts/measure-benchmarks.mjs。页面上的日期就是这份数据的生成日期。
测量方法: each timing is the median of 3–5 runs on an otherwise idle machine;Node crypto over a single 64 MB random buffer, digest included;random bytes encoded with Buffer.toString("base64");zlib.gzipSync at level 6;synthetic record arrays, JSON.parse and JSON.stringify timed separately;pdf-lib creating pages with one text run each, then copyPages + save;canvas.toBlob inside headless Chromium; a 1600×1200 gradient with 4000 noise specks.