雜記 › 技術筆記 › 10 萬個鍵全進 DOM:JSON 檢視的渲染規模、爆棧深度與複製重排

10 萬個鍵全進 DOM:JSON 檢視的渲染規模、爆棧深度與複製重排

先把四個能重現的數擺出來

同一份構造資料,鍵數分別是 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,元素數約等於鍵數的四倍,這是實作方式決定的算術,不是優化與否的問題。

實測耗時 ms12.5102.2938.61853.91000 鍵10000 鍵100000 鍵200000 鍵

圖 1:耗時隨鍵數線性上升,200000 鍵仍然全量進 DOM。

鍵數格式化耗時容器內元素
100012.5 ms4007
10000102.2 ms40007
100000938.6 ms400007
2000001853.9 ms800007

沒有截斷上限,但首屏可能一條都看不見

「全量渲染」不等於「全量可見」。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 格式錯誤」五個字。真正的差別在於:語法錯誤是你改一個字元就能修的,而爆棧是遞迴實作撞到了引擎堆疊上限,同樣深度換個進入點可能就過了。看到這句話時,先懷疑層數,別懷疑逗號。

2000 層1 次爆棧4000 層0 次爆棧6000 層10 次爆棧另一路徑臨界值 2886 2896 2930每檔同深度連測 10 次

圖 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 空格或 Tab 字元縮排的輸入也會被統一成這個樣式。左側同名按鈕複製的才是原文,兩個按鈕名字一樣、產物不一樣。它還會順手還原轉義:{"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 的那個;需要對比縮排時,先比字元數再比內容。

📎 相關文章

文字比對的代價全在 (m+1)×(n+1) 這張表:6000 行還能跑,8000 行直接堆疊記憶體崩潰 2026-10-28 帶 g 的正規表示式是有狀態的:同一個 RegExp 連打 4 次 test 只過 2 個 2026-10-28 27760000 個像素零差異、體積漲到 15.881 倍:JPG 轉 PNG 的三層位元組帳 2026-10-28 20 行檔名拆成 18 行兩列:貼進 Excel 只佔 1 列,1e5 變成 100000 2026-10-28 整個資料夾拖進來,候選池只多了 1 個名字:檔案隨機抽取讀不到目錄裡的 2558 個檔案 2026-10-28