JSONSchemaAlgorithmsPerformance

递归从入门到入土:JSON Schema 生成中的堆栈溢出与架构救赎

2026-03-228 分钟阅读

看到 RangeError 时,我的第一反应是:这届浏览器不行。本文复盘我如何把递归扔进垃圾桶,用显式栈结构搞定 150 层嵌套的 JSON Schema 生成。

糟糕的开头

RangeError: Maximum call stack size exceeded

看到这个报错的时候,我的第一反应是:这届浏览器不行。当时我正尝试给一个从大厂内部系统导出的复杂配置 JSON 生成 Schema,结果这个文件丧心病狂地套了 150 层。递归执行到一半,V8 直接把执行栈给掐了,浏览器标签页卡得像是在跑 2005 年的《魔兽世界》。作为 daima.life 的唯一首席开发者,我当时只想对着显示器比个中指。

我的思考

为什么市面上现成的生成器在面对这种“怪物”时全部秒怂?

原因很简单:大部分人的代码逻辑太“教科书”了。递归确实优雅,但在面对这种工业级的极端嵌套时,它就是个定时炸弹。传统的服务端工具可以通过调大进程栈内存来硬抗,但我的 daima.life 坚持“Privacy-First”,数据必须在纯前端处理。在 Cloudflare Pages 托管的静态环境下,我没有 Node.js 后端可以依赖,一切都得在用户的浏览器里完成。如果不能解决堆栈溢出,那我就得对用户说:“抱歉,你的 JSON 太深了,我吃不下。”

这怎么能忍?于是我决定重写整个推断引擎,把递归直接扔进垃圾桶,改用显式栈结构(Explicit Stack)。

技术硬核区

核心逻辑其实就是用「循环+数据栈」模拟「递归」。常规的递归生成逻辑大概是这样:

function infer(node) {
  if (isObject(node)) {
    return { type: 'object', properties: mapValues(node, infer) }; // 这里会产生恐怖的调用链
  }
}

改为栈结构后,我维护了一个状态队列。每个待处理的节点会被推入数组,我们通过循环不断处理数组顶端的元素,直到栈为空。

function generateSchema(input) {
  const root = { schema: {} };
  const stack = [{ data: input, parent: root, key: 'schema' }];
  

while (stack.length > 0) { const { data, parent, key } = stack.pop(); // 根据 data 类型生成局部 schema const currentSchema = inferSingleType(data); parent[key] = currentSchema;

// 如果是复杂对象,不递归,而是把子项压入栈
if (typeof data === 'object' && data !== null) {
  if (!currentSchema.properties) currentSchema.properties = {};
  for (const k in data) {
    stack.push({ data: data[k], parent: currentSchema.properties, key: k });
  }
}

} return root.schema; }

这样做的好处很明显:我们的对象深度不再受限于 V8 的虚拟机栈大小,而是受限于内存。除非你的 JSON 大到让浏览器直接崩掉,否则它再深也跳不出我的手掌心。

FAQ 模块

Q1: 栈结构相比递归,在处理循环引用时会有性能优势吗?

A: 本质上没有,但栈结构更方便我们使用 WeakSet 来存储已访问对象的标记。在循环中,我可以轻松地在状态里解耦引用检查和推断逻辑,避免递归回溯时丢失上下文。

Q2: 如何在栈结构中精确推断 1024 级深度的日期格式?

A: 这就是栈的强项了。我可以把正则探测器挂在一个全局的并发任务调度器里。当我们在栈中遇到字符串时,异步触发多正则并行匹配,甚至可以用 Web Worker 开启分线程。递归做这些异步操作会写出一堆地狱回调。

Q3: 为什么这种架构最适合 Cloudflare Pages?

A: 因为它是纯无状态的。我们不需要昂贵的 Edge Runtime 计算,直接把解析逻辑封装成一个不到 5KB 的 JS Chunk,CDN 秒开。用户的数据在浏览器内部自生自灭,这就是极致的安全感。

结尾

重写完引擎后,之前的 150 层嵌套现在只需不到 20ms 就能吐出完整的 JSON Schema。这就是独立开发的乐趣——遇到过气的教科书范式,就亲手把它拆了重造。不过,最近我发现有些大模型生成的 JSON 会带有非法的尾随逗号和控制字符,下一波我要瞄准这个点,搓一个容错率高到离谱的“暴力解析器”...