encodeURIComponent 把 URL 撑大 1.9 倍、gzip 却砍到 1/9:单文件工具状态塞进 URL 的实测复盘 📅 发布时间:2026/8/30 4:30:20 👁 浏览次数: 单文件工具最迷人的卖点之一就是「把状态塞进 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. 经典派base64urlURL 安全的 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·gzipb64C÷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的长度比例。gzipbase64url绿从 1KB 起就掉到 0.34 并一路降到 0.08——即只有朴素写法的 8%–11%base64url蓝稳定在 0.7 左右。朴素写法本身因百分号膨胀始终最胖。结果长度与耗时两张账单长度账单crossover 出现在 0.09KB 与 1.02KB 之间。低于约 0.5KB 的微配置比如几个开关项gzipbase64url 反而比纯 base64url 略大0.09KB 时 1.04×gzip 头开销抵消了压缩收益但只要状态到 1KB 这一档一个只有 6 个节点的小图就已经 1KBgzip 就反超到 0.34×到 6.7KB 已是 0.16×之后稳定在 0.08–0.11×。换句话说对任何「像样」的工具状态gzipbase64url 只有朴素写法 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三种写法的编码耗时纵轴对数。gzipbase64url绿在微配置上比 base64url蓝慢约 300×但绝对值仅 0.3ms随状态增大差距收敛到约 16×且全程异步、仅发生在「分享」动作上。局限到底什么时候该压缩不是所有状态都值得压。三类边界要诚实地记下来微配置0.5KB、低熵几个开关/筛选条件gzipbase64url 体积略亏、编码反而最慢。这种直接用 base64url甚至裸encodeURIComponent也装得下别为「优雅」硬上压缩。含密钥/隐私的状态URL 会被历史记录、代理、日志、剪贴板同步捕获永远别把 token、密钥、PII 塞进 hash。压缩不改变这一风险。超大文档型状态如整篇长文编辑。即便 gzip 压到 1MB也超过「随手分享」的体验阈值——此时正路是「导出文件」或配合 localStorage 缓存而非硬塞 URL。本次测量也只覆盖结构化、重复性高的工具状态未覆盖随机二进制或已加密负载那些 gzip 几乎无收益。另外原生CompressionStream需运行环境支持现代浏览器与 Node 18 已普遍支持老 Safari 需 fflate 之类轻量 polyfill 兜底上线前请确认目标环境的兼容矩阵。结论与下一步把状态塞进 URL 没有银弹但有一条可复用的取舍线默认用base64url一旦状态超过约 1KB 就切到原生CompressionStream的 gzipbase64url不到 1KB 的微配置不必压。而那个被无数教程首推的encodeURIComponent(JSON.stringify(state))除了改动最小在体积膨胀 1.9×和越线风险上都是三者里最吃亏的——它只适合「临时看看、不分享」的场景。开源地址单文件工具矩阵门户https://github.com/wangzifan396-wzf/WB单文件工具聚合器https://github.com/wangzifan396-wzf/nano-workbenchGitHub 组织主页https://github.com/wangzifan396-wzf