核心原則

監聽器的「觸發頻率」是效能的第一道防線、跨多種 listener 類型一起盤點。 本篇是 audit 視角(「我有效能問題、reactive 監聽器是不是嫌疑」)— 設計新 observer 的細節由 #29 MutationObserver 範圍與觸發頻率 處理。Audit 時把所有 reactive 監聽器列一張表、看哪些觸發頻率異常。

本篇焦點:跨 listener 類型的效能盤點

  • MutationObserver 的設計細節(root / option / debounce / self-mutation)由 #29 處理
  • Selector 範圍的設計#14 處理
  • Runtime 計算成本(regex / textContent / forEach)由 #34 處理

為什麼觸發頻率主導效能

商業邏輯

Reactive 監聽器有三個獨立成本:

成本來源單次量級累積方式
觸發頻率看範圍與 option倍數疊加
Callback 內部運算看實作每次完整跑
Callback 引發的副作用看 DOM 變動可能反向觸發

把單次 callback 從 5ms 優化到 2ms 是 2.5x;把觸發次數從 100 次/秒降到 10 次/秒是 10x。觸發頻率優化的天花板更高 — audit 時優先看頻率。

三類觸發頻率風險(速覽)

類型表現詳細處理
範圍過寬(observer subtree)無關變動也觸發#29 root 與 option 設計
Option 全勾多種變動類型同時觸發#29 三維度收斂
自激迴圈callback 自己改 DOM 觸發自己#29 self-mutation 處理

本篇不展開設計細節(避免跟 #29 重複)、只談「audit 時怎麼識別這些 risk」。


跨 observer 類型的盤點

效能 audit 時、列出所有 reactive 監聽器、不只 MutationObserver。各類型觸發來源不同、需要分別評估。

類型觸發來源過頻訊號
MutationObserverDOM 變動一次操作觸發 10+ 次
ResizeObserver元素尺寸變動持續觸發(自激)/ resize 視窗時連發
IntersectionObserver可視性變動scroll 時連發
Event listener (input / scroll / resize)使用者互動高頻事件未 debounce
setInterval / requestAnimationFrame 迴圈時間持續跑、不只在需要時

盤點工具

DevTools Performance 面板錄一段使用者操作、看 callback 觸發次數:

 1// 在 callback 內加 console.count
 2new MutationObserver(function (mutations) {
 3  console.count('mutation observer fired');
 4  // ... 處理
 5}).observe(...);
 6
 7new ResizeObserver(function (entries) {
 8  console.count('resize observer fired');
 9  // ... 處理
10}).observe(...);

跑一次「使用者打字 + 等結果」的完整操作、看 console 各 listener 觸發幾次。

觸發次數評估
1-3 次正常
5-10 次可能過頻、值得查
10+ 次範圍 / option 太寬、需要收斂
持續觸發(不停)自激迴圈、需要立刻處理

ResizeObserver 寫變數造成自激

ResizeObserver 的特殊風險是「寫 CSS 變數可能影響被觀察元素自己的尺寸」 — 這個 case 跟 #29 處理的 MutationObserver self-mutation 機制不同、值得獨立展開。

機制

1function syncScopeHeight() {
2  document.documentElement.style.setProperty(
3    '--search-scope-h', scopeEl.offsetHeight + 'px'
4  );
5}
6new ResizeObserver(syncScopeHeight).observe(scopeEl);

如果 --search-scope-h 在 CSS 中被用來計算 scopeEl 自己的 padding / margin / height — 寫入觸發 layout、layout 觸發 resize、resize 觸發 callback、callback 又寫入。

症狀

  • CPU 持續被佔
  • Performance 面板看到 ResizeObserver callback 連發(>60/秒)
  • 元素尺寸持續微調

解法

結構分離:寫的變數不該影響被觀察元素自己。

1new ResizeObserver(syncScopeHeight).observe(scopeEl);
2// scopeEl 高度寫到 --search-scope-h
3// CSS 中 --search-scope-h 用來計算 drawer 的 margin-top
4// drawer 不是 scopeEl、不會反向觸發

設計時讓「觀察的元素」跟「受變數影響的元素」結構上分離 — 不會循環。

跟 MutationObserver self-mutation 的差異

觀察類型self-mutation 機制處理
MutationObservercallback 改 DOM 結構 / attributedisconnect + observe 配對
ResizeObservercallback 改變數 → 反向影響尺寸結構分離(觀察 A、影響 B)
IntersectionObservercallback 改可視性 → 反向觸發罕見、設計時避免

ResizeObserver 沒有 disconnect 配對的等價技巧(disconnect 後再 observe 仍會立即重觸發) — 必須靠結構分離。


盤點的標準格式

每個 reactive 監聽器寫成一段註解、audit 時讀這份「設定卡」即可:

 1/**
 2 * 監聽:.pagefind-ui 的子節點變動
 3 * 類型:MutationObserver
 4 * 範圍:subtree(深層也看)
 5 * Option:childList only
 6 * Callback 是否改 DOM:是(toggle class)
 7 * 自激風險:否(class change 不觸發 childList)
 8 * Debounce:80ms
 9 * 預期觸發頻率:使用者打字一次 < 5 次
10 */
11new MutationObserver(schedule).observe(ui, { childList: true, subtree: true });

audit 時、看註解就知道:

  • 這個 observer 在做什麼
  • 預期觸發頻率多少
  • 實測超過預期 → 範圍太寬或 option 過勾

設計取捨:頻率管理策略選擇

當盤點發現某個 observer 觸發過頻、四種應對:

A:縮 observer 範圍 / option(這個專案的預設)

  • 機制:subtree → 直接子;移除沒用的 option flag
  • 選 A 的理由:成本最低、改一行;觸發頻率倍數降低
  • 適合:絕大多數過頻 case
  • 代價:需要重新確認哪些變動類型真的需要監聽
  • 詳細#29 三維度收斂

B:加 debounce / throttle

  • 機制:高頻觸發合併成低頻 apply
  • 跟 A 的取捨:B 不解問題的根(觸發仍發生)、A 解根;但 B 對「無法縮範圍」的 case(如 input event)必要
  • B 比 A 好的情境:使用者輸入事件、scroll 事件 — 本身高頻、無法縮範圍

C:Disconnect / reconnect 配對

  • 機制:callback 改 DOM 前 disconnect、改完 reconnect
  • 跟 A/B 的取捨:C 處理 self-mutation、A/B 不處理;C 比 A/B 複雜
  • C 比 A/B 好的情境:MutationObserver callback 必須改 DOM(沒有結構分離選項)
  • 詳細#29 self-mutation 處理

D:ResizeObserver 結構分離

  • 機制:觀察 A、影響 B(B ≠ A)
  • 跟 C 的取捨:ResizeObserver 沒 disconnect 等價技巧、必須用 D
  • D 是 ResizeObserver 自激的唯一解

不該套用「頻率管理」的情境

不是所有 reactive 監聽器都需要管:

情境為什麼可以放任
開發階段、不上 production效能不影響真實使用者
Callback 極輕(單次 < 0.1ms)觸發 100 次也才 10ms
觸發頻率本來就極低(一次 setup 一次 callback)沒有頻率問題

核心判準:實測有效能問題嗎?沒有就不必預先優化。Audit 是「找已存在的問題」、不是「預防所有可能」。


跟其他原則的關係

關係
#29 MutationObserver 範圍與觸發頻率互補 — #29 是設計指引(怎麼寫 observer)、本篇是 audit 視角(怎麼找問題)
#14 Selector 精準度跟 observer 範圍同源 — selector 起點就是 observer root 的選擇基礎
#34 Runtime 計算成本互補 — 本篇看「觸發次數」、#34 看「單次 callback 成本」
#43 最小必要範圍「縮監聽範圍」是「最小必要範圍」原則的應用

判讀徵兆

訊號該檢查的位置
使用者操作後瀏覽器卡頓該操作觸發了哪些 observer、各自觸發次數
CPU 持續 100%observer 自激迴圈(特別是 ResizeObserver)
setTimeout(0) 也來不及處理observer / event 觸發頻率超過 schedule 處理速度
Callback 內加 console.count 數字爆炸observer 範圍過寬 — 收斂方式由 #29 處理
ResizeObserver 在某 callback 後持續觸發寫的變數反向影響觀察元素 — 結構分離

核心原則:reactive 監聽器的效能 audit = 列所有 listener + 量觸發次數 + 比對預期。發現問題後、設計修正方式由 #29 等設計指引篇展開 — 本篇只負責「找問題」這一步。