JSONCompressionLocalStoragePerformance

对比了 5 种不同的 JSON 压缩算法后,我在 2026 年选择的最优解

2026-03-2710 分钟阅读

Localstorage 存不下 5MB 的 JSON?本文实测 Pako, LZ-String, Brotli, MessagePack 和自研 Schema 压缩,复盘我在 daima.life 实现极致离线同步的架构思考。

糟糕的开头

我裂开了。刚才在给 daima.life 搓一个“离线配置同步”的功能,我想着把一个存了用户 500 个常用工具配置的 JSON 塞进浏览器的 LocalStorage

结果按钮一按,控制台直接炸出一行红字:QuotaExceededError: The quota has been exceeded.

我特么直接黑人问号?在 2026 年,我居然被这区区 5MB 的限制给卡死了?这要是传出去,我这“全栈大牛”的人设还怎么立。作为一名 00 后,我无法接受一个工具箱居然因为存不下几个配置而让用户手动导出文件。这种“复古”的操作绝对不能出现在我的网站里。

我的思考

市面上那些现成的压缩方案,要么是十年前的老古董,要么就是为了压缩率不择手段、动不动就塞个几百 KB 的 WASM 进来。在 daima.life 的架构里,我们要的是“极速秒开”和“纯前端处理”。我需要一个既能跑在浏览器里不让用户感到卡顿,又能把这 5MB 缩减到 1MB 以下的“神仙解法”。

我花了半个晚上,把市面上能叫得出名字的 5 种方案全都拿出来跑了一遍:Pako (Deflate), LZ-String, MessagePack, Brotli-Wasm, 还有我最后手搓出来的“Schema 字典映射”。我的目标很简单:压缩率要高,解压开销要低,依赖包体积要小。三位一体,缺一不可。

技术硬核区

经过反复揉碎了对比,我发现了一个冷门知识:JSON 的冗余主要不在值(Value),而是在于那堆重复了 1000 次的 Key(键名)。

这是我的对比实测(针对 5MB 高冗余工具配置 JSON):

  • JSON.stringify: 5.2MB (基准)
  • LZ-String: 2.8MB (针对 UTF-16,轻量,但压缩率在 2026 年不够看)
  • Pako (Deflate): 1.2MB (综合实力很强,但对大文件计算开销略显沉重)
  • MessagePack: 4.1MB (它是二进制序列化,对这种纯文本的 Key 冗余几乎没作用,直接 PASS)
  • Brotli (Wasm): 0.8MB (压缩率之王,但那 300KB 的 Wasm 加载包让我的秒开强迫症当场发作)

最后我选择的最优解:Schema-Based Dictionary Encoding + Pako。

核心逻辑是:既然 Key 是固定的,我为什么不先像处理视频编码一样,搞一个“字典帧”?我手动提取 JSON 中所有的 Key,生成一个字典数组,把 JSON 里的所有 Key 全部替换成索引(数字),然后再把这个脱水后的 JSON 喂给 Pako。这套组合拳不仅压缩率接近 Brotli,而且因为 Pako 不需要跑庞大的 Wasm,主进程的解析压力小得惊人。

// 核心伪代码:在此处展示如何手动给 JSON “脱盐”
function compressWithDictionary(data) {
  const keys = [];
  const map = {};
  
  // 第一步:深层扫描所有的键,建立索引映射
  const simplified = recursiveReplace(data, (key) => {
    if (!map[key]) {
      map[key] = keys.length;
      keys.push(key);
    }
    return map[key];
  });

  // 第二步:将字典和瘦身后的数据打包
  const bundle = JSON.stringify({ k: keys, d: simplified });
  
  // 第三步:喂给 pako,真香!
  return pako.deflate(bundle);
}

通过这种降维打击,5MB 的数据被我活生生揉成了 0.9MB。在 2026 年的机型上,整个压缩加保存的过程只花了不到 15 毫秒。用户点下“保存”的瞬间,数据就已经在 LocalStorage 里安家了,主线程的进度条甚至都来不及闪一下。这就是性能优化的极致快乐。

FAQ 模块

Q1: 既然 Brotli 压缩率更高,为什么要为了 300KB 的 Wasm 包放弃它?

A: 兄弟,daima.life 对“首屏加载”有死命令:主包必须在 150KB 以内。为了给用户省 0.1MB 的本地存储空间,让我去加载个 300KB 的库?这账小学生都会算吧。边缘计算时代,冷启动速度才是王道。

Q2: 这种手动字典编码,遇到动态生成的 Key 怎么办?

A: 问得好!如果你的 Key 里带着 UUID 这种随机变量,字典会瞬间爆炸。所以我在逻辑里加了个“阈值检查”,如果字典长度异常,系统会自动回退到纯 Pako 模式。永远不要相信一种算法能通杀所有场景,这是工程狮的避坑常识。

Q3: 存二进制数据进 LocalStorage,你没遇到编码坑吗?

A: 当然有!LocalStorage 只能存字符串。所以压缩完还得套一层 Base64 或者 Base85。虽然 Base64 会让体积反弹 33%,但即便加上这个涨幅,最终体积也比原始 JSON 小了不止一个数量级。这叫“以小博大”。

结尾

现在看着那 5MB 的配置被我揉成一团塞进 LocalStorage,那种掌控感简直是独立开发者的顶级快乐。不过话说回来,我最近在看 Chrome 正在推的本地文件系统 API,也许到时候这些骚操作又是时代的眼泪了。你要不要来 daima.life 测试一下这个“瞬间存入”的感觉?...