FumadocsZDecode
JavaScript

大 JSON 的处理方案

一、问题在哪

JSON.stringify(hugeObj) // 同步,阻塞主线程
JSON.parse(hugeString) // 同步,阻塞主线程

// 100MB 的 JSON → 主线程卡死几秒,页面无响应

二、方案一:丢给 Worker

// 主线程
const worker = new Worker('json-worker.js')

worker.postMessage({ type: 'stringify', data: hugeObj })

worker.onmessage = (e) => {
  console.log(e.data) // 序列化后的字符串
}
// json-worker.js
self.onmessage = (e) => {
  const { type, data } = e.data
  if (type === 'stringify') {
    self.postMessage(JSON.stringify(data))
  } else if (type === 'parse') {
    self.postMessage(JSON.parse(data))
  }
}

postMessage 传大对象本身需要结构化克隆,也有开销。如果是字符串,用 Transferable 传 ArrayBuffer 实现零拷贝:

// 主线程 → Worker 传大字符串
const encoder = new TextEncoder()
const buffer = encoder.encode(hugeJsonString).buffer
worker.postMessage(buffer, [buffer]) // 零拷贝转移

// worker.js
self.onmessage = (e) => {
  const str = new TextDecoder().decode(e.data)
  const obj = JSON.parse(str)
  self.postMessage(obj)
}

三、方案二:流式解析

浏览器端 — fetch ReadableStream

async function parseHugeJson(url) {
  const response = await fetch(url)
  const reader = response.body.getReader()
  const decoder = new TextDecoder()
  let jsonStr = ''

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    jsonStr += decoder.decode(value, { stream: true })
  }

  // 读取过程不阻塞,但最后 parse 还是同步的
  return JSON.parse(jsonStr)
}

真正的流式解析需要 oboe.js,边解析边回调,不用等全部加载完:

oboe('/api/huge-data.json').node('users[*]', (user) => {
  processUser(user)
  return oboe.drop // 处理完立即释放内存
})

Node 端 — stream-json

const { parser } = require('stream-json')
const { streamArray } = require('stream-json/streamers/StreamArray')
const fs = require('fs')

fs.createReadStream('huge.json')
  .pipe(parser())
  .pipe(streamArray())
  .on('data', ({ value }) => processItem(value))

// 内存始终只保留当前那一条

四、方案三:分片 + requestIdleCallback

function stringifyChunked(arr, chunkSize = 1000) {
  return new Promise((resolve) => {
    const chunks = []
    let i = 0

    function processChunk() {
      const slice = arr.slice(i, i + chunkSize)
      chunks.push(JSON.stringify(slice).slice(1, -1)) // 去掉 []
      i += chunkSize

      if (i < arr.length) {
        requestIdleCallback(processChunk) // 浏览器空闲时处理下一块
      } else {
        resolve(`[${chunks.join(',')}]`)
      }
    }

    processChunk()
  })
}

五、方案四:换二进制格式

JSON 对大数据天然不友好,考虑换格式从根本上解决:

// MessagePack — 比 JSON 小 30-50%,编解码更快
import { encode, decode } from '@msgpack/msgpack'

const packed = encode(hugeObj) // → Uint8Array
const obj = decode(packed)
格式体积速度可读性
JSON✅ 可读
MessagePack小 30-50%快 2-3x❌ 二进制
Protobuf最小最快❌ 需要 schema

六、方案选择

场景推荐方案
前端处理几十 MB JSONWorker + Transferable
前端展示超大列表流式解析(oboe.js)+ 虚拟列表
Node 处理 GB 级文件stream-json 流式处理
数据不算大但不想阻塞 UIrequestIdleCallback 分片
前后端都能改换 MessagePack / Protobuf

七、一句话总结

大 JSON 的核心问题是 parse/stringify 同步阻塞。优先用 Worker 移出主线程;超大文件用流式解析边读边处理;阻塞不严重时用 requestIdleCallback 分片;前后端都能改则换二进制格式从根本上降低体积和解析耗时。

On this page