病理報告 · 04 事件處理未節流

引用約定(本檔所有數字都必須能循此找到出處): records[mode,run].欄位docs/measurements/2026-07-26-reproducibility-4x-5.jsonspecimenId = "04-unthrottled-events" 的那 12 筆(正典檔,isFullSweep: trueproblems: [])。phase2:Ndocs/phase2-expected-results.md 第 N 行; 行號會漂,引用時連同內容一起引(phase2:1199 的教訓)。 04.ts:Nspecimens/04-unthrottled-events.ts 第 N 行。

判準凍結聲明:依 phase2:1164(§四)與 phase2:1334-1338(追記「生效範圍」), #4 的比值判準對本份 JSON 不回溯適用。本報告對臂間關係只報方向與絕對值, 不寫「通過/未通過比值判準」。新判準(broken / fixed-raf ≥ 2×,phase2:1267) 自下一輪量測起才生效。

04 事件處理未節流(Unthrottled Event Handlers)

病症一句話:CDP 以「十拍、每拍連滾三格滾輪」驅動 8000 列清單捲動時, 每次 scroll / wheel 事件都做一整輪 8000 次 getBoundingClientRect()(每趟約 36ms,4x), 5 秒量測窗內掉幀峰值 99~119 幀(約佔 300 幀窗的三分之一);把計算移進 rAF 後 掉幀約砍半再多一點(median 113 → 42),換成 IntersectionObserver 後剩 10~16 幀 —— 但該臂本輪 unstable,絕對值不可靠。

⚠️ 這些絕對值是本 session、本機、4x 宣告節流下的值。未飽和臂的掉幀數 是機器速度的函數(phase2:1280、phase2:1085-1090 在 #6 上的同型證據), 跨 session 只有方向可信,絕對值不可比(phase2:944-945)。

凍結條件

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

CPU throttle 4x —— 宣告值,JS 偵測不到(records[*].cpuThrottle;sweep 頂層 cpuThrottlingRate: 4
驅動方式 CDP 機器驅動(sweep 頂層 driverInput.dispatchMouseEvent,非人手)
viewport 800 × 600(src/specimens.ts:299FROZEN_VIEWPORT
refreshHz 60(records[*].refreshHz,12 筆皆同)→ 5 秒窗約 300 幀
操作程序 wheel 連滾 3 格(每格 deltaY 120,tools/reproducibility.mjs:166,187,341)× 10 拍 · 間隔 500ms 絕對排程src/specimens.ts:283-296
節拍護欄 dispatchSpanMs 4999.5 ~ 5014.9(名目 5000,records[*].dispatchSpanMs / dispatchSpanNominalMs)——最大偏差 0.3%,節奏側 channel(phase2:601-611 的缺陷 C)未復發
buildId 0.1.0-ms191c71records[*].buildId,12 筆皆同)
protocolVersion records 未帶此欄(本份 JSON 的 record 結構內沒有它,不編造)
負載規模 N = 8000 列(04.ts:54;登記值 2000,經 phase2:342-359 校準,內容總高現行有效值 384,000px,phase2:1110)

動工前登記的預期

登記出處:原始登記 phase2:59-122(2026-07-25);重新設計後的再登記 phase2:624-633(2026-07-26);判準定案追記 phase2:1267-1283(同日稍晚)。 這一節不准回頭改去迎合實測。 修正只在該檔「修正紀錄」追加,此處照錄並標注作廢狀態。

實測

三輪,同一組凍結條件。離散度算在每輪的主指標終值上,不是 max—— protocol.ts:290:「抗離群。可重現性判定用這個,不用 max」。 相對離散度 = (max − min) ÷ median-of-three(每格都按此式計算)。 主指標 custom.droppedFramesPeak;本輪協定十拍 × 500ms 正好填滿 5 秒滾動窗, 所以每筆 droppedFrames(終值)與 droppedFramesPeak 逐筆相等。

mode 三輪掉幀峰值 相對離散度 絕對全距 判定
broken 每次事件全掃 119 / 113 / 99 17.7% 20 幀 ✅ 可重現
fixed-passive 只翻 passive 116 / 112 / 105 9.8% 11 幀 ✅ 可重現
fixed-raf rAF 節流 48 / 42 / 37 26.2% 11 幀 ✅ 可重現
fixed-observer 換 IntersectionObserver 11 / 16 / 10 54.5% 6 幀 unstable(擦線)

(出處:records[mode,run].custom.droppedFramesPeak 全 12 筆; fixed-observer 的 54.5% 與 phase2:1071 的補記逐字一致。)

判準:相對離散度 ≤ 30%,絕對全距在雜訊底線內(掉幀底線 5 幀,phase2:448-454)。 fixed-observer 全距 6 幀比底線多 1 幀、離散度 54.5% 超過 30%——兩條都沒過,擦線判 unstable。 絕對差 6 幀是 300 幀窗的 2%,屬於「治療越有效越判它不可重現」的已知型態 (phase2:878-880 在上午資料上的同一裁定:判準就是判準,不因為擦線就放它過)。

護欄逐項(全部在正典 JSON 內部核對):

護欄 broken fixed-passive fixed-raf fixed-observer
rectReads ÷ passes 8000 / 8000 / 8000 8000 / 8000 / 8000 8000 ×3 —(rectReads = 0)
wheelDeltaTotal(px) 3600 ×3 3600 ×3 3600 ×3 0(必為 0:不掛 wheel listener,04.ts:133
scrollTop 終值(px) 3600 ×3 3600 ×3 3600 ×3 3600 ×3
wheelCancelableCount 21 / 20 / 20(> 0) 0 ×3 0 ×3 0 ×3
rafSkipped 0 ×3 0 ×3 20 / 20 / 22 0 ×3

另計(非主指標):INP 12 筆皆無互動樣本(records[*].totalInteractions = 0、 inp = null——未量測,規格排除捲動);LCP 12 筆逐位元相同(728ms,元素 precords[*].lcp)——A 類不重載 document,這是首載定格值,不隨 mode 變, 不構成臂間比較項;CLS 12 筆皆 null = 無 entry(不是量到 0)。

兇手歸因

broken fixed-passive fixed-raf fixed-observer
inputDelay 未量測(無互動樣本) 未量測 未量測 未量測
processing 未量測 未量測 未量測 未量測
presentation 未量測 未量測 未量測 未量測
LoAF forcedPeak(僅標本 script) 548.7 / 2.9 / 2.8 ms 3.5 / 3.5 / 3.6 ms 1.9 / 1.2 / 1.5 ms 無 entry / 0 / 無 entry
loafPickedBy forcedStyleAndLayout ×3 forcedStyleAndLayout ×3 forcedStyleAndLayout ×3 none / specimenScriptDuration / none
sourceFunctionName (空字串) / scanOnEveryScroll / scanOnEveryWheel scanOnEveryScroll / scanOnEveryWheel / scanOnEveryScroll scanOnAnimationFrame ×3 無 entry ×3

(出處:records[mode,run].forcedPeak / .forcedFn / .loafPickedBy。 INP 三段皆 null——依模板前言寫「未量測」,不寫 0。)

三輪兇手段是否一致:INP 三段無資料可判——這不是缺陷,是本標本的規格 (捲動不產生 interactionId,phase2:107-108)。登記的兇手 loaf 依 phase2:1174 從未被判準機制驗證,本報告的歸因走掉幀 + 護欄計數器,LoAF 只是佐證:

presentation 繼承 duration 的 8ms 量化、blockingDuration 含外殼—— 這兩條模板警語對本標本無用武之地(INP 段全部未量測),照錄以示未刪節。

治療梯度

梯度形狀先講清楚04.ts:19-28,366-385):前三臂是同一機制下的三格變因隔離 (broken → 只翻 passive 旗標 → 只翻「計算在哪裡跑」),第四臂是換機制, 與前三臂不共用任何變因,只與 broken 比,不與 fixed-raf 排名(phase2:637-639)。

治療 掉幀峰值(三輪 / median) 相對病變(方向與絕對值) 代價
fixed-passive 只翻 passive: true 116 / 112 / 105(112) 無改善:median 113 → 112,臂間差 1 幀,遠低於底線 5 幀 承諾不 preventDefault();換到的是本尺量不到的 compositor 側收益
fixed-raf 計算移進 rAF 48 / 42 / 37(42) 改善:113 → 42,比值 2.7×(113 ÷ 42 = 2.69);passes 41→14、rectReads 328k→112k 計算晚一幀(pending 狀態管理);每次執行仍是全量 8000 次讀取
fixed-observer 換機制 11 / 16 / 10(11) 方向大幅改善、絕對值不可靠:median 比 10.3×(113 ÷ 11),但該臂 unstable(54.5%);最保守配對 99 ÷ 16 = 6.2× 換掉整個機制:rectReads 歸 0,但回呼語意變了(只報跨界列);進臂要跑 8000 次 observer.observe()

無效的治療照實寫:治療一在主指標上無效,而這個無效是登記在案的預期結論—— 一個沒效的治療比三個有效的更值得寫,它說明 passive 的成本假設落在讀者以為的位置以外。

與登記的差異

處置順序照模板:① 查環境(本輪 dispatchSpanMs 全部貼著名目、sweep problems: [], 無環境異常記錄)② 換更精確的驅動方式(絕對排程已是現行)③ 動 protocol——本輪不需要。

誠實揭露