☰
encodeURIComponent 把 URL 撑大 1.9 倍、gzip 却砍到 1/9:单文件工具状态塞进 URL 的实测复盘
2026/10/29 5:31:23 网站建设 项目流程

单文件工具最迷人的卖点之一就是「把状态塞进 URL 片段,复制即分享」——没有后端、没有数据库,用户把地址栏一复制,整张画布就跟着过去了。可真到上线那天,你大概率会撞见两件事:要么状态稍大一点,分享出去的链接长到自己都懒得看;要么在手机上粘贴时被某些 App 拦腰截断。问题往往出在序列化这一步——而大多数教程默认给你的那行encodeURIComponent(JSON.stringify(state)),其实恰好是三种常见写法里最不划算的一个。

背景:单文件工具的「URL 片段状态」模式

把应用状态编码进location.hash(即#之后)是单文件 Web 应用(SFWA)的经典做法:hash 不会发给服务器,静态托管零成本,复制 URL 即复现状态。Mermaid Live Editor 就是这么干的——它把编辑器状态序列化、压缩、再编码进 URL,让一张图能被一条链接完整带走。

但「序列化」这一步有不止一种写法,代价也完全不同。我们横评三种在单文件工具里最常见、也最容易被随手选用的方案:

  • A. 朴素派:encodeURIComponent(JSON.stringify(state))——几乎所有「快速上手」教程的默认写法,改动最小。
  • B. 经典派:base64url(JSON.stringify(state))——用 base64url 做可移植编码,避免百分号把链接撑爆。
  • C. 压缩派:base64url(gzip(JSON.stringify(state)))——借浏览器/运行时的原生CompressionStream先压再编码。

图1:同一个state对象,经 JSON 序列化后分三条通路——A 走encodeURIComponent(因对{ " : , }等结构字符百分号编码,体积反而膨胀);B 走base64url(固定 +33% 开销);C 先经原生CompressionStream压缩再base64url,体积最小。

解剖:三种写法到底差在哪

先给出可落地的实现(Node 22 / 浏览器均原生支持CompressionStream):

// A. 朴素派 const encA = (json) => encodeURIComponent(json); const decA = (s) => JSON.parse(decodeURIComponent(s)); // B. 经典派(base64url,URL 安全的 base64) const encB = (json) => Buffer.from(json, 'utf8').toString('base64url'); const decB = (s) => JSON.parse(Buffer.from(s, 'base64url').toString('utf8')); // C. 压缩派(原生 gzip + base64url) async function encC(json) { const gz = await new Response( new Blob([json]).stream().pipeThrough(new CompressionStream('gzip')) ).arrayBuffer(); return Buffer.from(gz).toString('base64url'); }

三者的本质区别只有一句话:A 把 JSON 里的结构字符{ " : , }也做了百分号编码,所以体积不是「原样」,而是被凭空撑大了约一倍;B 是固定 +33% 的 base64 开销;C 用压缩把重复结构吃干抹净,再付一次 base64 的 33% 税。真正要回答的是:这三者差出来的体积和耗时,在真实工具状态量级下到底有多大?

实证:0.09KB 到 7.3MB 的真实测量

为贴近单文件工具的日常形态,我构造了一个「图编辑器」状态样本(节点带x/y/w/h/label/color/parent/collapsed/tags等高度重复的字段,gzip 友好),按节点数从 6 个放大到 4 万个,覆盖 0.09KB–7.3MB 的原始 JSON。每种写法各跑 11–21 轮取中位数,gzip 异步写法含 2 轮预热。环境:Node v22.22.2。

# 复现:在产出目录运行 node bench_url_state.js # 输出 bench_url_state.json

实测结果(「编码后长度」即最终塞进 URL 的字符数;耗时含编码与解码两段):

规模(raw)A·encURIB·b64urlC·gzip+b64C÷B 长度C÷A 长度
0.09 KB184 B126 B131 B1.04×(略胖)0.71×
1.02 KB1.94 KB1.36 KB0.47 KB0.34×0.24×
6.73 KB12.8 KB8.98 KB1.43 KB0.16×0.11×
103.7 KB194 KB138 KB17.0 KB0.12×0.09×
887 KB1.60 MB1.16 MB133 KB0.11×0.08×
7.28 MB13.0 MB9.48 MB1.03 MB0.11×0.08×

一个横贯全表的常量值得先标出来:A 写法把 JSON 撑大了 1.83–1.96 倍(中位约 1.9×),因为它的百分号编码把每一个{ " : , }都变成了%7B %22 %3A %2C %7D。也就是说,「最朴素」的写法,反而是体积最胖的那一个。

图2:相对朴素写法encodeURIComponent的长度比例。gzip+base64url(绿)从 1KB 起就掉到 0.34 并一路降到 0.08——即只有朴素写法的 8%–11%;base64url(蓝)稳定在 0.7 左右。朴素写法本身因百分号膨胀始终最胖。

结果:长度与耗时两张账单

长度账单:crossover 出现在 0.09KB 与 1.02KB 之间。低于约 0.5KB 的微配置(比如几个开关项),gzip+base64url 反而比纯 base64url 略大(0.09KB 时 1.04×,gzip 头开销抵消了压缩收益);但只要状态到 1KB 这一档(一个只有 6 个节点的小图就已经 1KB),gzip 就反超到 0.34×,到 6.7KB 已是 0.16×,之后稳定在 0.08–0.11×。换句话说:对任何「像样」的工具状态,gzip+base64url 只有朴素写法 8%–11% 的体积,且比 base64url 也小 2.9–9.2 倍。

这张账单还直接撞上 URL 的实用天花板。分享链接的 hash 虽不发往服务器(不受 8KB 代理限制约束),但浏览器与「复制粘贴」体验的实际可用上限约在 64KB 左右。于是 103KB 原始状态那档:朴素写法产出 194KB 的 URL(早已越线、分享即废),gzip 写法只有 17KB(稳稳装下);到了 7.28MB 那档,朴素写法给出 13MB 的链接(荒诞),gzip 仍把体积压在 1MB 级。

耗时账单:gzip 的代价在「编码」一侧。编码耗时(中位):micro 0.33ms、1KB 0.18ms、6.7KB 0.23ms、103KB 1.03ms、887KB 7.05ms、7.28MB 54.3ms;而 base64url 对应仅 0.001–3.3ms,朴素写法 0.001–32.7ms。两个要点:(1)在微配置上 gzip 编码比 base64url 慢约 300 倍,但绝对值只有 0.3ms,可忽略;(2)gzip 是异步非阻塞的,且只在用户点「复制分享链接」那一刻发生一次,并不会像「每次输入都同步写 URL」那样卡住主线程。解码侧三者同量级(7.28MB 档:base64url 37ms、朴素 54ms、gzip 56ms),差距在十毫秒级,不构成瓶颈。

图3:三种写法的编码耗时(纵轴对数)。gzip+base64url(绿)在微配置上比 base64url(蓝)慢约 300×,但绝对值仅 0.3ms;随状态增大,差距收敛到约 16×,且全程异步、仅发生在「分享」动作上。

局限:到底什么时候该压缩

不是所有状态都值得压。三类边界要诚实地记下来:

  • 微配置(<0.5KB、低熵):几个开关/筛选条件,gzip+base64url 体积略亏、编码反而最慢。这种直接用 base64url(甚至裸encodeURIComponent也装得下),别为「优雅」硬上压缩。
  • 含密钥/隐私的状态:URL 会被历史记录、代理、日志、剪贴板同步捕获,永远别把 token、密钥、PII 塞进 hash。压缩不改变这一风险。
  • 超大文档型状态:如整篇长文编辑。即便 gzip 压到 1MB,也超过「随手分享」的体验阈值——此时正路是「导出文件」或配合 localStorage 缓存,而非硬塞 URL。本次测量也只覆盖结构化、重复性高的工具状态,未覆盖随机二进制或已加密负载(那些 gzip 几乎无收益)。

另外,原生CompressionStream需运行环境支持(现代浏览器与 Node 18+ 已普遍支持;老 Safari 需 fflate 之类轻量 polyfill 兜底),上线前请确认目标环境的兼容矩阵。

结论与下一步

把状态塞进 URL 没有银弹,但有一条可复用的取舍线:默认用base64url,一旦状态超过约 1KB 就切到原生CompressionStream的 gzip+base64url;不到 1KB 的微配置不必压。而那个被无数教程首推的encodeURIComponent(JSON.stringify(state)),除了改动最小,在体积(膨胀 1.9×)和越线风险上都是三者里最吃亏的——它只适合「临时看看、不分享」的场景。

开源地址:

  • 单文件工具矩阵门户:https://github.com/wangzifan396-wzf/WB
  • 单文件工具聚合器:https://github.com/wangzifan396-wzf/nano-workbench
  • GitHub 组织主页:https://github.com/wangzifan396-wzf

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询