你能读多大的 JSON 文件?我试了 500MB,发现 JSON.parse 会直接让页面崩溃
你能读多大的 JSON 文件?我试了 500MB,发现 JSON.parse 会直接让页面崩溃
导语
做数据看板或日志查看器时,很多人第一个想到的就是:把后端给的 JSON 一整个 JSON.parse 扔进内存。可如果这个文件是 100MB、300MB、甚至 500MB,页面很可能直接卡死、白屏,甚至标签页崩溃。这篇文章用一次真实试错,说清楚为什么大 JSON 会把页面卡死,以及三个你当天就能落地的解法。
正文
很多人以为“JSON 只是一段文本”,所以 JSON.parse 应该很轻。实际上,JSON.parse 在解析时会做以下几件事:
- 递归把字符串展开成完整对象
- 分配大量临时对象和数组
- 触发主线程长任务,浏览器输入/动画全部卡住
- 内存峰值翻倍:文本本身 + 解析后的对象图
我拿 100MB、300MB、500MB 三个 JSON 做了一次简单测试,结果很直接:
- 100MB:主线程卡 400~700ms
- 300MB:标签页开始掉帧,部分浏览器直接假死
- 500MB:Chrome 直接弹“页面无响应”,最终崩溃
说白了,问题不是 JSON 本身,而是 JSON.parse 是一次性、同步、阻塞主线程的操作。
方案一:先问自己,真的需要全部加载吗?
很多时候,大 JSON 的瓶颈是“我要的只有 20 条,却拿回了 500MB”。优先从数据源头解决:
- 服务端分页、按需字段过滤
- 支持范围请求,只取当前页
- 换更紧凑的格式,如 protobuf / msgpack / flatbuffers
这是成本最低的方案,但很多时候改不了后端。那就看前端怎么处理。
方案二:流式读取,不要一次性读全文
浏览器其实已经有现成的流 API。这里最实用的是 fetch + ReadableStream,因为不需要任何库,所有现代浏览器都支持。
以日志查看器为例,如果你想边下边看,可以这样:
async function streamJson(url) { const response = await fetch(url); if (!response.ok) throw new Error(`请求失败:${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let depth = 0; let inString = false; let escapeNext = false; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); for (let i = 0; i < buffer.length; i++) { const ch = buffer[i]; if (inString) { if (escapeNext) { escapeNext = false; continue; } if (ch === '\') { escapeNext = true; continue; } if (ch === '"') inString = false; continue; } if (ch === '"') { inString = true; continue; } if (ch === '{' || ch === '[') depth++; else if (ch === '}' || ch === ']') depth--; if (depth === 0 && (ch === '}' || ch === ']')) { // 已拿到可解析的 JSON 片段 } } buffer = buffer.slice(Math.max(0, buffer.length - 200)); }}
这段代码的核心不是完美解析,而是告诉你一个思路:流式处理大 JSON 的关键,是把“全文解析”拆成“边读边剪”。
方案三:用更适合大文件的解析策略
如果你必须在前端拿到完整结果,至少也分三步走:
- 先测内存:解析前先判断文件大小
- 小文件直接
JSON.parse - 大文件走 Web Worker,避免阻塞 UI
async function safeParseJson(text) { const sizeMB = new Blob([text]).size / 1024 / 1024; if (sizeMB > 50) { const blob = new Blob([` self.onmessage = (e) => { try { const data = JSON.parse(e.data); self.postMessage({ ok: true, data }); } catch (err) { self.postMessage({ ok: false, error: err.message }); } }; `], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); const result = await new Promise((resolve) => { worker.onmessage = (e) => resolve(e.data); worker.postMessage(text); }); worker.terminate(); if (result.ok) return result.data; throw new Error(result.error); } return JSON.parse(text);}
这个函数很适合包装成一个工具函数:小文本直接解析,大文本自动进 Worker。
结论:不要默认“前端能直接吃下任意 JSON”
这次 500MB 测试最大的收获不是代码技巧,而是认知:
- 能用流,就不要全量读
- 能用服务端过滤,就不要把大包甩给前端
- 能分页,就不要一次全给
如果你正好在做日志查看器、数据导出、报表系统,或者任何“后端一甩就是几十 MB”的场景,先把这三点想清楚,再决定要不要 JSON.parse。
收尾
下次遇到“页面一加载就崩”的数据场景,先问三个问题:
1. 我真的需要全量数据吗?
2. 服务端能不能分片/过滤?
3. 前端是不是该用流或 Worker?
把这三个问题想清楚,比直接换一个解析库更重要。
评论区
登录后可评论。













