前端效能病理標本館 · 第三篇 · 入口總覽 本文不產生新數字。每個數字都引自已發出的兩篇文章、六份病理報告或
docs/的登記檔, 逐處標明來源與 session。已發出的文章不回頭改數字(本站規矩), 所以本文引用它們時,連同「後來被翻案」的部分一起引。
我蓋了一座前端效能的病理標本館。六個標本,每個是一個效能反模式: 同一頁可以切換病變版與治療版,旁邊的面板顯示實測指標。
它是一份實驗,不是一個應用程式。整座館只做一種宣稱:
其他條件全部凍結時,翻動這一個變因,產生這個差距。 (
perf-pathology-museum-spec.md§1)
這篇是入口,回答三個問題:為什麼是標本館,不是再一篇技巧文; 這裡的數字憑什麼可信;你能從這裡拿走什麼。
網路上的效能文章大多長這樣:「長列表請使用虛擬滾動」,附一張優化前後的截圖,講完了。
本站也量了虛擬滾動。第一輪的結果是 LCP 1360 → 680ms,2.0 倍,發表在第一篇文章裡
(《十二段治療處方,站得住的有四段》,原始資料
docs/measurements/2026-07-25-reproducibility-4x.json)。
後來查出來,真實答案是 23.3 倍(第二篇,2026-07-26-reproducibility-4x-5.json)。
差在哪?不在標本——標本一個字都沒改。差在量測方法:切換 mode 時,
前一份 document 的拆除工作落在新 document 的時鐘裡,LCP 因此帶著一個由
「前一個 mode 有多重」決定的加項。同一輪之內實測,這個污染項約 790ms,是訊號的八倍;
最刺眼的一組對照是 141 個節點的 document,載入耗時是 40,021 個節點那份的五倍
(DCL 8,5279,553ms 對 8191,907ms,第二篇第五節)。
這個缺陷沒有症狀,而且它讓數字更漂亮:污染是常數,常數不製造離散度, 於是那一輪標本 #2 的離散度 2.9%,全站最漂亮的數字之一。 單次量測的截圖抓不到這種錯;連「連跑三輪」都抓不到—— 一個穩定的錯誤,可重現性判準對它一無所知(第二篇第五節)。
第二個例子更直接。第一輪量測跑完時,面板上 12 段治療處方有 11 段看起來都在改善。
拿判準和護欄計數器逐段過濾之後,效果既乾淨又可歸因的只剩 4 段(第一篇)。
有的治療同時翻了兩個變因,有的從未進入作用區間,
有一段的核心程式碼從未執行過——rendersSkipped 三輪恆為 0,
要不是去看這個護欄計數器,它可以永遠躲著(第一篇第二節)。
只看面板數字的話,這 12 段每一段都可以寫成一篇像樣的技巧文。
所以差別不在結論,在機制:技巧文發表之後就結束了; 標本館發表之後,錯誤還會被自己的流程抓回來,翻案紀錄跟原始結論擺在一起。 第一篇的多處數字後來失效了,但那篇文章一個字不改——它是有日期、附原始資料連結的快照, 第二篇講的正是它哪裡錯了、為什麼錯。
六個標本(specimens/),每個是獨立頁面,外殼用 iframe 載入,面板顯示實測指標。
標本分兩類,這個分法會影響整條量測路徑:
館裡還有一個不算標本的東西:校準件(specimens/00-calibration)。
它的每個負載都有解析解可以反推——忙迴圈 300ms,Event Timing 的 processing 段就該量到 300.x。
實測 300.8~303.1ms(docs/HANDOFF.md,路由對調後的煙霧測)。
它的功能是回答一個技巧文從來不問的問題:憑什麼相信量測層自己沒說謊?
驗收也是實跑出來的:tools/acceptance.mjs 用真實瀏覽器派送真實輸入跑 13 條檢查
(合成的 element.click() 沒有 interactionId,Event Timing 一筆都不回報,
所以必須走 CDP 的真實輸入)。這個 repo 沒有單元測試框架,驗收就是測試套件。
不是因為沒出過錯。出過的錯兩篇文章加起來寫了三萬多字。 可信的理由是:每一道防線都在檔案裡留下它攔到了什麼。
viewport 凍結在 800×600(CLS 與 LCP 都是 viewport 相對量);build 設定固定; 零外部請求——不准 CDN 字體、不准第三方分析(spec §4.7), 量測站點自己去要外部資源,等於把污染源寫進實驗器材。
凍結不了的就宣告:CPU throttle 從 JS 偵測不到,所以 4x 是宣告值, 印在面板和每一筆資料裡,不假裝是偵測值。驅動方式也宣告—— 全部數字是 CDP 機器驅動,不是人手,這件事每篇文章的頭三行都寫。
每個標本動工前,預期的數字、兇手段、風險先寫進
docs/phase1-expected-results.md / docs/phase2-expected-results.md。
登記完不准回頭改——要改只能在檔尾追加修正紀錄,寫明改了什麼、作廢哪些數字。
量完再寫預期的話,預期永遠是對的,這個檔案就沒有存在意義。
「可重現」不是形容詞,是三條機械判準:三輪比值落在同一個量級帶、三輪兇手段一致、 病變值相對離散度 ≤ 30%(spec §1,原則 4)。
比值判準有四條是後來補定案的,定案過程值得單獨講,因為它誠實到有點難堪:
定案時已經看過兩輪實測,無法假裝沒看過。所以防線改成——每個數字都附推導、
推導不引用「剛好過關」的落點,而且其中一條(#4 的 ≥ 2×)在已看過的資料上
邊際只有 12%,下一輪落空是真實可能。挑數字的人若在迎合實測,
不會挑一條可能打臉自己的線(docs/phase2-expected-results.md 檔尾追記「比值判準四條定案」)。
下一輪掃描(2026-07-26-reproducibility-4x-6.json)四條首次生效,四條全過——
包括那條邊際最薄的,實測 3.5×(phase2 檔尾「儀器修正後的完整掃描」)。
機器負載會讓數字撒謊:同一套驗收,load < 2 時 13/13 全綠,
load 3.75.8 時掉到 811,失敗項還會游移(CLAUDE.md)。
所以量測工具開跑前檢查 load,≥ 2 直接拒跑;每一筆 record 都帶 loadAvg1m 欄位
(phase2「儀器修正五條」)。現行正典掃描全程 load 0.82~1.61,寫在檔頭——
以後「是機器還是標本」看欄位,不用吵。
量到不符預期,改的是實驗設計,不是把結論改成符合實測。 這條規矩的必然結果,就是下一節那些翻案——它們不是防線失守的證據, 是防線在工作的證據。
第一篇判了八段治療有問題。修掉實驗設計的缺陷、重量一輪之後,八格翻了六格—— 而且往四個不同的方向翻(第二篇):
歸零 #4 passive 0.63× → 1.0× 隔離變因之後療效完全消失
從無到有 #4 rAF 節流 沒發生 → 2.7× 合併閘門先前一次都沒觸發過
變大 #1 scheduler.yield 17× → 32× 混淆變因移除,效果反而更大
沒動 #6 批次化 1.04× → 1.2× 進了作用區間,還是零效果
「混淆變因一定在幫治療臂作弊」是個直覺,而這一輪有兩格是反過來的:
工作量對齊之後 scheduler.yield 從 17 倍變 32 倍,量測污染修掉之後虛擬滾動從 2 倍變 23.3 倍。
混淆變因往哪個方向偏,不能用直覺猜(第二篇第四、五節)。
標本 #1(主執行緒阻塞)的兇手歸因,在較慢的機器狀態下三輪一致是 presentation;
機器變快之後變成 presentation / inputDelay / inputDelay——三輪不一致,判準亮警示。
質量在兩段之間整塊搬家(同一輪內:run1 presentation 951ms / inputDelay 0.8ms;
run2 presentation 120ms / inputDelay 798ms),不是雜訊抖動
(docs/phase1-expected-results.md 修正紀錄「標本 #1 兇手改判為交界標本」)。
機制:INP 看不看得見排隊,不取決於有沒有排隊, 取決於排隊期間瀏覽器有沒有機會畫。這支標本正好卡在那條界線上, 兇手是機器速度的函數,不是標本的性質。
裁決有三條,每一條都是誠實原則的實作:登記值不改(規矩 2),改成明文例外;
兇手宣稱換成合成判準(inputDelay + presentation 合計 918~952ms,壓倒 processing);
分析器的警示保留不消——讀者應該看到它翻,然後被指到這一條裁決。
交界本身就是展品,不是要修掉的缺陷。
第二篇量到:把混淆變因隔離之後,passive: true 的掉幀 113 → 112,歸零。
但那篇文章特意留了一句邊界警語:這把尺量的是主執行緒的出幀節奏,
量不到 passive 真正買的東西(compositor 側不等 handler 就能先捲)——
「量到一個零」是尺的解析度邊界的結論,不是「passive 沒用」的一般結論(第二篇第二節)。
同一天晚上的下一輪掃描(-4x-6.json),這一臂三輪穩定優於病變版 7~8 幀
(92/94/91 對 99/102/99),超出 5 幀的同格帶——
登記的「fixed-passive 不得優於 broken」落空。
可比性護欄全部對稱(wheelEvents 六筆皆 20、passes 兩臂同帶、load 同帶),
「臂間工作量不對稱」和「機器狀態」兩個便宜解釋都被排除
(phase2 檔尾「#4 fixed-passive 方向性判準落空」)。
處置:落空照實記,判準不改。機制假說(passive 省下的是等待、不是工作,
所以先前判它「不該有可量的效果」的推理可能才是錯的那個)明文標示未驗證,
留給下一輪先驗機制再談改登記。可能錯的是登記時的推理,不是資料。
一個判準的價值,恰恰在於它落空時你敢不敢把落空印出來。
下表的實測全部出自同一份正典原始資料
docs/measurements/2026-07-26-reproducibility-4x-5.json(4x 宣告節流、CDP 機器驅動、
800×600、每 mode 三輪),完整推導在 docs/reports/01~06 六份病理報告,
每個數字在報告裡都附了 JSON 欄位路徑。
| 標本 | 病變(一句話) | 病變實測 | 治療實測 |
|---|---|---|---|
| #1 主執行緒阻塞 | 十連發點擊,click handler 同步排序五萬筆訂單,把主執行緒整段佔住 | INP 三輪 median 1028 / 896 / 796ms | 切 chunk + 讓出:24~32ms |
| #2 長列表 | 5000 列一次渲染成 40,021 個 DOM 節點,最大那塊文字的繪製被擋住 | LCP 1864ms | content-visibility 520ms(3.6×);虛擬滾動 80ms(23.3×,只建 141 個節點) |
| #3 強制同步版面 | 800 列交替「讀 offsetHeight、寫 width」,每列都逼瀏覽器重算版面 |
forced median 829~964ms | 讀寫分離:三輪零強制版面幀 |
| #4 未節流事件 | 每個 scroll/wheel 都做一整輪 8000 次 getBoundingClientRect() |
掉幀峰值 99~119 | rAF 節流:median 113 → 42 |
| #5 版面位移 | 零互動,三個位移源(無尺寸圖、字族換入、插入橫幅)落進同一個 session window 累加 | CLS 0.2694(三輪逐位元相同) | 逐一預留空間:一筆 layout-shift entry 都沒有 |
| #6 重繪風暴 | 每 25ms 推一批更新,每批重建整張 1000 列清單 | 掉幀峰值 275~281(300 幀的窗,幾乎每幀都掉) | 只改變動的節點:14~18,幀距貼回 16.7ms |
三條必讀的警語,六份報告各自展開:
這部分跟前端無關,量任何東西都用得上。
1. 主指標之外要有護欄計數器。主指標告訴你快了多少,
護欄計數器告訴你這個數字能不能拿來說話(第一篇第七節)。
layoutChecksum 兩臂逐位元相同,證明治療沒偷工——順帶一則本篇才更正的錯:
第一篇印的 checksum 值 54168 其實出自另一支 1x、四次點擊的校準探針,
三份正典 JSON 的 18 筆實測全部是 54888(報告 03)。已發文章不回頭改(規矩 5),
所以更正寫在這裡——護欄計數器連自己被引錯都有辦法被抓到,因為每個值都有出處可對;
rendersSkipped 恆為 0,
抓到一段從未執行的程式;passes 與 rectReads 逐輪相同,抓到兩臂做著同一件事。
第一篇抓出那八段有問題的治療,靠的全部是這種計數器(第一篇第七節)。
**2. 先量你自己的雜訊底線。**一段位元級相同的路徑跑六輪,掉幀全距 5 幀(第一篇第二節)—— 這就是本站的尺。小於底線的差距一律不宣稱;「治療臂 A 比治療臂 B 好 1 幀」在這裡不是句子。
3. 相對離散度在治療有效時會把方向搞反。(max−min)/median 在 median 趨零時爆炸:
掉幀 4/1/1 算出 300%,判不可重現——治療越成功,越判它不可重現(第一篇第六節)。
判準需要一條絕對底線,不能只有相對值。
**4. 可重現性判準抓不到穩定的錯。**它只看三輪之間一不一致,
一個每輪都在的常數偏差,它給你全站最漂亮的離散度。
要抓它,得有一條直接問「同一個 mode 量兩次,數字一樣嗎」的測試
(tools/b-class-isolation.mjs,第二篇第九節)——而它本來應該從第一天就存在。
**5. 解析度下限印在 UI 上,不放在心裡。**Event Timing 的 durationThreshold 最低 16ms、
duration 四捨五入到 8ms、LoAF 的 blockingDuration 是整幀的——三條全部印在面板與文章裡
(spec §1 誠實原則)。連「標本 #2 的 LCP 標的其實是『請不要動』那段操作說明、
不是那 5000 列清單」這種會讓故事變醜的事實,也照實登記成凍結變因並寫進報告
(phase2 檔尾追記、報告 02)。標明限制之後,就大方地下結論。
/measure.html。每個標本頁都能切病變/治療,面板即時顯示指標。
量測功能需要 Chromium 系瀏覽器(interactionId 與 LoAF 都是 Chromium-only)。
面板出現紅色 banner 表示在 dev server 上——那時的數字不算數。docs/reports/01~06):每份八節,凍結條件、兇手歸因、
治療梯度、誠實揭露,每個數字附欄位路徑。docs/measurements/*.json):每篇文章頭三行都寫了它用哪一份,
可以自行覆算。臂間比值只在同一份 JSON 內部成立。docs/phase*-expected-results.md):預期、實測、修正紀錄、作廢清單。
想看「登記完不准回頭改」實際上長什麼樣子,看檔尾那一串追記。docs/HANDOFF.md 部署一節。