病理報告 · 02 長列表未虛擬化

docs/pathology-report-template.md(版型八節,一節不刪)。 實測數字取自現行正典檔 docs/measurements/2026-07-26-reproducibility-4x-5.json (2026-07-26T05:52:29.671Z、CDP 機器驅動、66 筆完整掃描、problems: [] 無異常輪、 isFullSweep: true;正典地位的裁定在 phase2:1057-1062「才是現行的正典檔」)。 歷史數字一律以「已作廢」或「另一 session」身分出現並標明來源 —— 跨 session 絕對值不可比(phase2:944-945),臂間比值只在同一份 JSON 內部成立。 行號會漂(phase2:1195-1199),本報告引用行號時儘量連同內容一起引。

02 長列表未虛擬化

病症一句話:4x CPU 節流下首載,一次把 5000 列渲染成 40,021 個 DOM 節點, 把整份 document 首屏最大那塊文字的繪製(LCP)拖到 1864ms(三輪 median); content-visibility 一行 CSS 降到 520ms(3.6×),虛擬滾動降到 80ms(23.3×)—— 而兩段治療真正的差別在節點數:40,021 個節點還在 vs 只建 141 個

⚠️ 這句話的 LCP 標的不是那 5000 列清單,是頁面上「請不要動」那段操作說明 (裸 p#specimen-root 第 4 個子元素,769 × 48 @ (8, 129),size 36672 —— phase2:1201-1213 追記)。本標本量的是「說明文字多久畫得出來,而清單把它擋住了」, 不是「清單自己成為 LCP」。這個差別寫在兇手歸因一節,讀者靠它判斷能不能複製這個實驗。

凍結條件

同一組條件之間才能比較。任何一項改動,先前所有數字作廢。

CPU throttle 4x(宣告值),JS 偵測不到(sweep.cpuThrottle: "4x"cpuThrottlingRate: 4;九筆 records 的 cpuThrottle"4x")。⚠️ B 類的宣告方式與 A 類不同:節流在導覽之前打開,並用 deep-link 的 cpu=4x 參數宣告,不按外殼下拉選單(tools/reproducibility.mjs:699-702, 714, 728-731——「載入後補按一次選單」的 evaluate() 會插進正在量的載入期)。代價:外殼 React bundle 也在 4x 底下解析,那是每筆樣本相同的常數
驅動方式 CDP 機器驅動(sweep.driver)。每一筆樣本都從一次全新的 measure.html 導覽開始?specimen=&mode=&cpu=),目標 mode 就是首載的那一份 document(tools/reproducibility.mjs:672-731)。捲動用 Input.dispatchMouseEvent 的 mouseWheel 一拍一格(realWheel:166;#2 沒有 wheelTicks 欄位,走逐格路徑,位元與舊版相同,:520-525
viewport 800 × 600FROZEN_VIEWPORTsrc/specimens.ts:10LONG_LIST_META.viewport:235)。⚠️ 實際內部寬度是 785——垂直捲軸吃掉 15px(phase2:41-42「這批標本一律不准寫死 800」),列的行數是寬度敏感的
refreshHz 60(九筆 records 的 refreshHz 皆 60;欄位定義 src/protocol.ts:68
操作程序 動作 scroll · 次數 10 · 間隔 500mssrc/specimens.ts:227-233)。外加兩段靜置:navQuietMs = 20000(從導覽起算,蓋住外殼在 4x 底下的開機)與 quietMs = 12000tools/reproducibility.mjs:296-299)。整段靜置一次 CDP 呼叫都不准發:733-735——輪詢等於把觀測動作放進被觀測的視窗)
buildId 0.1.0-ms191c71(九筆 records 的 buildId
protocolVersion 凍結契約常數 1src/protocol.ts:8)。⚠️ 正典 JSON 沒有記錄這個欄位(頂層無 conditions,驅動器也不寫入)—— 這一格是宣告值,不是記錄值。見「與登記的差異」末尾

這一份標本額外的凍結條件:

動工前登記的預期

登記出處:docs/phase2-expected-results.md:179-231(標本 #2 段)。 修正出處:phase2:326-340(煙霧測試)、:539-545(節點數計數 bug)、:936-940(判 unstable)、 :951-1044B 類前導污染,本標本最重要的一節)、:1119-1129(裁決紀錄 §二,兩條明文作廢)、 :1201-1232(LCP 標的追記)。 這一節不准回頭改去迎合實測。 要修正就在該檔「修正紀錄」追加。 #2 不在「比值判準四條定案」(phase2:1259-1338)的凍結範圍內(那是 #4 / #5 / #6), 以下登記區間仍可據以判定,但逐條先過修正紀錄確認現行狀態。

實測

三輪,同一組凍結條件。離散度算在每輪的 median 上,不是 maxsrc/protocol.ts:309 「抗離群。可重現性判定用這個,不用 max」)。LCP 是 per-document 單值,每輪一筆、 該筆即該輪 median。相對離散度 = (max − min) / median-of-three。

mode 三輪 LCP(ms) 相對離散度 絕對全距 判定
broken 全部渲染 1864 / 1788 / 1896 5.8%(108 ÷ 1864) 108ms ✅ 可重現
fixed-content-visibility 472 / 548 / 520 14.6%(76 ÷ 520) 76ms ✅ 可重現
fixed-virtual 虛擬滾動 84 / 76 / 80 10.0%(8 ÷ 80) 8ms(< 底線 50ms) ✅ 可重現(全距在雜訊底線內)

出處:records[mode=broken,run=1..3].lcp.valuerecords[mode=fixed-content-visibility,run=1..3].lcp.valuerecords[mode=fixed-virtual,run=1..3].lcp.value。三臂數字與 phase2:1065 補記的 「02 broken 1864/1788/1896 (5.8%) cv 472/548/520 (14.6%) virtual 84/76/80 (10.0%▽)」逐位元一致。

判準:相對離散度 ≤ 30%,或絕對全距在該指標雜訊底線內(LCP 50ms)。三臂皆過。 ⚠️ 只看相對離散度會把方向搞反:治療有效正是讓分母趨零 —— fixed-virtual 的 10.0% 全靠 分母只有 80ms,它的絕對全距 8ms 才是「穩」的證據。

次要指標(終值快照;M1 缺口的補法就是為了讓零互動的 B 類標本也能入帳,phase2:48-55):

mode domNodeCount renderedItems droppedFramesPeak cls
broken 40,021 / 40,021 / 40,021 5000 ×3 0 / 0 / 0 無 entrynull ×3)
fixed-content-visibility 40,021 / 40,021 / 40,021 5000 ×3 0 / 0 / 0 無 entrynull ×3)
fixed-virtual 141 / 141 / 141 15 ×3 9 / 9 / 10 無 entrynull ×3)

出處:records[*].custom.domNodeCount / .renderedItems / .droppedFramesPeakrecords[*].cls

兇手歸因

登記兇手 lcpphase2:216)。INP 三段結構性無樣本,表格照模板列出、如實填「無樣本」:

broken fixed-content-visibility fixed-virtual
inputDelay 無樣本(null,零互動) 無樣本 無樣本
processing 無樣本 無樣本 無樣本
presentation 無樣本 無樣本 無樣本
LoAF forcedStyleAndLayout 無強制版面樣本forcedSamples: []forcedMedian: null 同左 無 LoAF 幀loafPickedBy: "none"forcedPeak: null
loafPickedBy specimenScriptDuration(備援路徑) specimenScriptDuration none
specimenScript(備援選出的幀) 172.5 / 156.8 / 162.0 ms 159.3 / 191.6 / 160.2 ms 無(null ×3)
sourceFunctionName 無(forcedFn: "" 無(forcedFn: null

出處:records[*].forcedSamples / .forcedMedian / .forcedPeak / .loafPickedBy / .specimenScript / .forcedFn。 依模板規則,loafPickedBy 不是 forcedStyleAndLayout 的幀時,forcedFn 不構成 「有強制版面幀」的證據 —— 本標本三臂沒有任何一幀是靠強制版面選出來的。 病變不走強制重排路徑(那是標本 #3 的病),它走的是初次渲染把首屏繪製整段推遲

LCP 標的的身分phase2:1201-1232 追記,2026-07-26 解決 :338-340 登記的待辦):

三輪兇手段是否一致:(描述子層級:lcp.el 九筆皆 p;三臂三輪的差異全部呈現在 lcp.value 上)。但要註記:tools/analyze-repro.mjs 的「三輪一致兇手 vs 登記值」判準 值域涵蓋不到 lcpphase2:1166-1180:#2 落在 not-an-inp-segment,不報警)—— #2 登記的兇手從來沒有被任何自動判準驗證過,上面的「一致」是人工從 records 讀出來的。

presentation 繼承 duration 的 8ms 量化,會落在 8ms 網格上。 對量級對照無影響,只在替兩個都已經很快的方案排名時咬人(本標本兩段治療刻意不排名)。 blockingDuration 是整幀的值,含外殼,規格上無法拆到單一 script; forcedStyleAndLayoutDuration 是逐 script 的,所以只有它能乾淨濾掉外殼貢獻 —— 本標本兩者都沒有進入判定(無強制版面樣本),列出是為了讓讀者確認「沒有」。

治療梯度

治療 LCP(三輪 median) 相對病變 代價
一:content-visibility: auto + contain-intrinsic-size: auto 120px 520ms 3.6×(1864 ÷ 520) 40,021 個節點還在 DOM 裡domNodeCount 與病變逐位元相同);必須配對 contain-intrinsic-size,忘了就把渲染成本換成 CLS(specimens/02-long-list.ts:247-257);需要瀏覽器支援(mode requires: ['content-visibility']src/specimens.ts:216
二:虛擬滾動(可視範圍 ± 5 列) 80ms 23.3×(1864 ÷ 80) 節點只建 141 個,但換來真實的複雜度:Ctrl+F 找不到沒渲染的列、無障礙樹不完整、錨點失效(specimens/02-long-list.ts:155-162散文宣稱非量測);scroll listener + spacer 維護;捲動期掉幀 9 / 9 / 10(本輪唯一超過 5 幀底線的臂records[mode=fixed-virtual,run=1..3].custom.droppedFramesPeak

兩臂 LCP median(520 / 80ms)都在雜訊底線 50ms 之上,比值可報。 治療版落進雜訊底線時不報比值 —— 本標本沒有觸發這條,但 fixed-virtual全距(8ms) 已在底線之下,再快一個檔次這個比值就該收起來了。

兩段治療之間不排名(登記 R3,phase2:231)。毫秒數上 80 < 520,但排名毫無意義: 登記明文說本標本最重要的是節點數之差(phase2:218-221),而那一欄是 40,021 vs 141(283.8×,40021 ÷ 141)renderedItems 5000 vs 15 —— content-visibility 是「一行 CSS、結構一字不動」(病變與治療一共用同一支 renderEverything(),差別只有一個 class,specimens/02-long-list.ts:141-153), 虛擬滾動是「根本不建那些節點」,各自的代價欄不同,讀者按自己的相容性與複雜度預算選。

無效或反向的治療:無 —— 但治療二在次要指標上有一筆反向(捲動期掉幀 9/9/10 vs 病變的 0/0/0),照實列在代價欄。它是 renderWindow() 每次捲動重建視窗的真實成本, 不是雜訊(三輪 9/9/10,離散度 (10−9)/9 = 11%,穩定重現)。

與登記的差異

逐條對照,三種結局分開寫:

處置順序固定:① 查環境 ② 換更精確的驅動方式 ③ 最後才動 protocol。 本標本這一輪正是按這個順序走過來的實例:上午判 unstable(broken 34.0%、 fixed-virtual 110.7%,phase2:936-940,該輪九筆已全數作廢 :1023)—— 查出來不是標本不穩,是量測方式不穩(B 類前導污染,:951-1015): iframe 與外殼共用 renderer 主執行緒,前一份 document 的拆除落在新 document 的時鐘內, 而 LCP 取 entry.startTime。修法(deep-link 全新導覽 + 節流前置 + 暖身丟棄)動的是 驅動方式(②),不是 protocol(③),標本一個字沒改。修完三臂離散度 5.8 / 14.6 / 10.0%。 汙染有多大,同一輪內的對照(tools/b-class-isolation.mjs,皆 4x):fixed-virtual 前導 fixed-content-visibility 時 888~892ms、deep-link 全新導覽 100 / 92 / 108ms —— 污染項約 790ms,是訊號的八倍phase2:1032-1037)。 ⚠️ 這個缺陷沒有症狀(舊迴圈讓前導恆定、污染成常數,三輪離散度照樣漂亮,:972-974)—— 任何人把 B 類改回「按按鈕切 mode」,它就無症狀地回來,防線只有回歸測試。

儀器帳(非標本缺陷)另兩筆: ① 舊工具註解曾記 fixed-virtual 32 / 32 / 40ms,與正式三輪差約三倍 —— 已查明是 前一版探針把節流放在 document 載完後才套,載入期實跑 1x,三倍正是節流率本身 (tools/reproducibility.mjs:692-694)。引用不標條件,等於拿 1x 的數字描述 4x 的結論。 ② 正典 JSON 沒有 protocolVersion 欄位:頂層鍵只有 measuredAt / driver / cpuThrottle / cpuThrottlingRate / runsPerMode / specimensCovered / isFullSweep / records / problems / consoleErrors,驅動器程式全文也不寫入該欄。凍結契約的常數在 src/protocol.ts:8, 但「JSON 值必須等於 1」這條驗證無從執行 —— 兩份 phase 檔都沒記,留給主執行緒。

誠實揭露