JSON 工具箱

大 JSON 解析很慢怎么优化

结论:优化顺序错了,换什么库都没用。先量出时间花在解析、渲染还是请求上,再按「减少解析次数 → 流式处理 → 挪到 Worker → 换二进制格式」逐级加码。

进阶约 5 分钟发布于 2026-10-30更新于 2026-10-30
大 JSON 解析的性能优化顺序

结论:先量出网络、解析与渲染各占多少时间,多数「解析慢」其实是渲染慢;优化按「减少解析次数 → 拆小响应体 → 流式解析 → 挪到 Worker → 换二进制格式」的顺序逐级加码,换库放在最后。

先别换库。多数「解析慢」在加一次 console.time 之后会发现是渲染慢。

第一步:量清楚时间花在哪

const startFetch = performance.now();
const response = await fetch(url);
const text = await response.text();
const startParse = performance.now();
const data = JSON.parse(text);
const startRender = performance.now();
render(data);

分四段记录:网络、text() 读流、JSON.parse、渲染。三段耗时占比不同,优化手段完全不同:

占比最高的是 真正的问题 对策
网络 响应体太大 字段裁剪、分页、gzip/brotli
JSON.parse 数据量本身大 减少解析次数、流式、Worker
渲染 一次性渲染上万节点 虚拟列表、分页渲染、折叠

JSON.parse 本身是 C++ 实现,速度不慢:现代浏览器解析 1MB JSON 大约 5–15ms,10MB 大约 80–150ms。如果你测出来 10MB 要 2 秒,那问题多半不在 parse。

第二步:减少解析次数

这是收益最高、最容易被忽略的一步。

  • 别反复解析同一份数据。 常见写法是每个组件各自 JSON.parse(localStorage.getItem(...)),十个组件就解析十次。提到模块级缓存一次。
  • 别用 JSON.parse(JSON.stringify(x)) 做深拷贝。 它会把整棵树序列化再解析,代价是数据量的两倍以上。结构化克隆 structuredClone 快得多。
  • 别在主线程解析完再传给 Worker。 postMessage 会再克隆一次,等于双倍开销。把原始文本传给 Worker,让它自己解析。
  • 按需取字段。 只需要三个字段时,后端就该裁剪(GraphQL、fields 参数),前端不必先拿全量再 map。

第三步:拆小响应体

  • 分页:?page=1&size=200,一次拿一屏的量
  • 游标:大数据集用 cursor 而不是 offset,避免深翻页
  • 字段裁剪:列表接口只返回摘要字段,详情单独请求

这一步的效果通常比任何前端优化都大,因为它同时减少了网络、解析与内存占用。

第四步:流式解析

数据必须整体处理、又确实很大时,用流式解析器逐块处理,避免把整个对象树建在内存里:

import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';

const pipeline = fs.createReadStream('big.json')
  .pipe(parser())
  .pipe(streamArray());

for await (const { value } of pipeline) {
  await handleOne(value);
}

Node 端常用 stream-json、JSONStream;浏览器端可以配合 fetch 的 response.body 逐块读取。代价是代码复杂度上升,且不能随机访问——只适合「逐条处理」,不适合「先看整体再决定」。

第五步:挪到 Worker

解析本身不慢,但会阻塞主线程,表现为页面卡住、动画掉帧。把解析放进 Web Worker:

const worker = new Worker(new URL('./parse-worker.js', import.meta.url), { type: 'module' });
worker.postMessage(text);          // 传文本,不传已解析对象
worker.onmessage = (event) => render(event.data);

注意传的是原始文本。传已解析的对象会触发结构化克隆,等于把开销翻倍。

第六步:换格式(最后考虑)

如果数据是内部系统之间传输、体积稳定在几十 MB,可以考虑:

  • NDJSON / JSON Lines:一行一条记录,天然支持流式
  • Protocol Buffers / MessagePack:体积和解析都更优,代价是失去可读性与通用性
  • 列式格式(Parquet):分析型场景,按列读取

这些都属于「换协议」,需要两端一起改,不到万不得已不必动。

别做的事

  • 用 eval 解析 JSON:安全风险,且不一定更快
  • 为了「优化」把 JSON 转成字符串再塞进 JSON:体积翻倍
  • 在主线程里循环几十万次 JSON.parse 小字符串:合并成一次解析

常见问题

如何判断瓶颈在解析还是渲染?

用 performance.now() 把请求、text() 读流、JSON.parse 与渲染分成四段计时,占比最高的那一段才是真正的瓶颈。现代浏览器解析 1MB JSON 约 5–15ms、10MB 约 80–150ms,如果 10MB 要 2 秒,问题多半不在 parse。

为什么说换库通常解决不了问题?

JSON.parse 本身是 C++ 实现,速度不慢,多数「解析慢」来自重复解析或一次性渲染上万节点。换库通常只能带来 10%–30% 的提升,而减少一次全量解析往往能省一半。

是否应该把解析放进 Web Worker?

当解析阻塞主线程、页面卡住或动画掉帧时应该放进 Worker。注意要传原始文本而不是已解析的对象,否则 postMessage 的结构化克隆会把开销翻倍。

相关工具

延伸阅读