先把四个能复现的数摆出来
同一份构造数据,键数分别是 1000、10000、100000、200000,点「格式化视图」实测 12.5 ms、102.2 ms、938.6 ms、1853.9 ms,容器内元素分别是 4007、40007、400007、800007 个,最后一条里 json-entry 有 200000 条。四个点连起来是一条直线,没有任何一段出现「渲染到前 N 条就停」的拐点——模板里也确实不存在这样的分支。每个条目固定生成 1 个 div 加 3 个 span,元素数约等于键数的四倍,这是实现方式决定的算术,不是优化与否的问题。
图 1:耗时随键数线性上升,200000 键仍然全量进 DOM。
| 键数 | 格式化耗时 | 容器内元素 |
|---|---|---|
| 1000 | 12.5 ms | 4007 |
| 10000 | 102.2 ms | 40007 |
| 100000 | 938.6 ms | 400007 |
| 200000 | 1853.9 ms | 800007 |
没有截断上限,但首屏可能一条都看不见
「全量渲染」不等于「全量可见」。100000 键那份数据渲染完时,默认状态是全部收起,首屏可见条目 0 个,容器 scrollHeight 只有 298 px——一百多万个字符已经变成 DOM,但页面只占一小截。点「全部展开」之后 scrollHeight 变成 2409443 px,按每行 24.09 px 折算,正好是 600 px 视口的 4015.7 倍。所以判断这个视图会不会吃力,看的是元素总量而不是滚动条长度;反过来,滚动条短也不代表数据没进来。要测的是 JSON 视图在多大输入上开始变慢,而不是它有没有偷偷截断。
深嵌套:合法的 JSON 被报成语法错误
规模之外还有第二个坎,而且更坑。构造纯嵌套的对象反复测,同深度连测 10 次的结果是:2000 层只爆栈 1 次,4000 层 10 次全通过,6000 层 10 次全爆。换一条调用路径,又测出 2886、2896、2930 三个不同的临界深度。也就是说阈值不是一个固定值,它在随调用栈上已经吃了多少帧浮动,同一段文本这次能过下次未必。
爆栈时页面写的是这一行:
❌ JSON语法错误:Maximum call stack size exceeded
这段输入本身语法完全合法,报错文案却把它归进「语法错误」,渲染区只剩「JSON 格式错误」五个字。真正的差别在于:语法错误是你改一个字符就能修的,而爆栈是递归实现撞到了引擎栈上限,同样深度换个入口可能就过了。看到这句话时,先怀疑层数,别怀疑逗号。
图 2:同一深度重复 10 次的爆栈次数,阈值随路径浮动。
重复键在视图里只剩一个值
{"a":1,"a":2,"b":3} 这种输入在语法上是合法的,解析时后一个 a 覆盖前一个。视图只剩 2 行,a 显示 2,徽标写 " 2项"——原始文本里那个被覆盖的 1 在这里没有任何痕迹,不提示、不高亮、也不留在折叠标题里。同类的小口径差还有空容器:{} 与 [] 没有「0项」徽标,模板第 105 行只输出一个空格,所以「有几项」这个数在空对象上是缺省的。核对键有没有被吞掉时,只能拿源文本对,不能拿视图对。
搜索按文本节点命中,计数还会少算
搜索走 TreeWalker 扫文本节点,不区分键名与值:搜「项」命中的是第 112 行的计数徽标,搜 { 命中的是括号本身。更需要注意的是计数:{"s":"aba"} 搜 a,页面上有 2 个高亮,结果栏写「1 个匹配」——第 251 行对每个节点只取第一个命中。规模数据也印证这条路径不便宜:在 100000 键的树上搜 key9 得到 11111 个匹配,耗时 1071.3 ms,容器元素从 400007 涨到 422229,多出来的就是那一万多个高亮节点。把匹配数当「命中处数」读之前,先想想它可能是少算的。
复制出去的是重新生成的文本
右侧那个「复制」走的是 JSON.stringify(jsonData, null, 2),不是把原文还给你。100000 键的源文本 1677781 字符,复制得到 2077782 字符,多出的 400001 字符全是 2 空格缩进和换行;原本用 4 空格或制表符缩进的输入也会被统一成这个样式。左侧同名按钮复制的才是原文,两个按钮名字一样、产物不一样。它还会顺手还原转义:{"s":"\u4e2d\u6587 \u0041"} 这 27 个字符会被复制成 17 个字符的「中文 A」。键序按解析顺序保留,不会被重排。至于介绍里提到的「路径复制」,模板里 UU.copy 只出现在第 327 行一处,这个动作目前不存在。
非法 JSON 给不给行列,取决于引擎
12 个非法样本里 10 个带回 position、line、column,能直接定位;但「Unexpected end of JSON input」和 5 行输入里的尾逗号只回显一段原文,一个字位置都不给。模板第 84–88 行只是把 e.message 拼上去,没有任何自己算行列的代码,所以定位粒度完全取决于 V8 走哪条分支,工具没有能力也没有承诺把它变精确。
显示层的数字:只做 String()
视图对数字做的只是 String():{"n":1e400} 在视图里显示 Infinity,复制出来是 null;12345678901234567890 显示成 12345678901234567000。值是在解析那一刻就被改掉的,改值的是 JSON.parse 和 JSON.stringify,不是视图。关于精度为什么会丢,旧文和别处都讲透了,这里不再重复。本篇只处理规模与显示层这一侧;至于折叠、搜索、复制这套操作怎么配合阅读,看《JSON 太长找不到字段?树形视图折叠和搜索定位》。
还可能想知道
Q:有没有一个安全的键数上限?
没有硬上限。1853.9 ms 渲染 200000 键是慢而不是断,真正会断的是嵌套深度,而且它连同一个深度都测不出稳定阈值。
Q:报 Maximum call stack size exceeded 是数据坏了吗?
不是。这个文案覆盖的是语法合法但层数过深的输入,同深度换路径重测有可能就过了。
Q:想拿到和粘贴时一模一样的文本怎么办?
用左侧那个同名按钮复制原文,别用右侧走 stringify 的那个;需要对比缩进时,先比字符数再比内容。