核心原則

「最小必要範圍」是 sanity 防線、不是優化選項。 縮 selector / observer / DOM 操作的範圍、目的不是為了讓程式跑更快、是為了讓行為可預測:不誤命中、不過度觸發、不被未來頁面結構變動打破。從具體放寬比從寬泛收緊容易得多 — 兩者的成本曲線完全不對稱。


為什麼是「sanity 防線」、不是「優化」

兩個概念常被混為一談

「縮範圍」聽起來像效能優化(少做一點工 = 快一點)— 這誤解掩蓋了它真正的價值。

維度優化Sanity 防線
目標提升某個量化指標(速度、記憶體)防止某類錯誤發生
衡量跑得多快、用多少資源行為是否可預測
失敗代價慢一點出錯時 debug 困難
該追求的時機有量到瓶頸時寫第一行就該追求

把「縮範圍」當優化的後果:以為「現在沒效能問題、之後再縮」 — 但 sanity 防線錯過了第一行就追求的時機、未來補救成本更高。

寬範圍的代價不是「慢」

寬 selector / observer / 操作範圍的失敗模式:

失敗表現Debug 難度
誤命中其他元素動了不該動的、且通常不報錯高 — 安靜失敗、bug 表現遠離 root cause
過度觸發apply 跑了 N 次、其中 N-1 次無意義中 — 看 callstack 不知道為什麼觸發
跟未來結構變動衝突加了一個 widget 後原本的程式壞掉高 — 不知道哪個假設被打破
跟 framework 渲染週期競爭在 layout 還沒穩時跑、視覺閃爍高 — 時序問題、難以重現

四種代價都不是「慢」 — 都是「行為不可預測」。Sanity 防線守的是這個。


「從具體放寬」vs「從寬泛收緊」的不對稱性

兩個方向在表面上對稱、實際成本曲線完全不對稱:

從具體放寬(推薦)

1寫第一版:用最具體的 selector / 最小的 observer 範圍 / 最窄的操作邊界
23發現某個 case 沒覆蓋
45顯式評估「該放寬到什麼程度」
67擴大範圍、知道擴大後的影響範圍

每次擴大都是顯式決定 — 知道「我為什麼擴大、擴大到哪」。

從寬泛收緊(反推薦)

1寫第一版:用最寬的 selector / subtree observer / 全頁面操作
23某天發現某個 bug(誤命中、過度觸發、framework 衝突)
45不確定哪些地方是「故意要寬」、哪些是「意外寬了」
67試著縮、可能漏掉某些故意要寬的場景、引發新 bug

收緊時面對的問題:寬範圍的程式碼裡看不出「哪些是故意寬、哪些是意外寬」。原作者也不一定記得當初為什麼寫寬。

不對稱性的根源

這個不對稱不是工程偏好、是資訊量的差異

  • 從具體放寬:每次擴大時、有當前需求當證據(「為了 X case 才擴大」)
  • 從寬泛收緊:縮的時候、不知道原本依賴哪些寬範圍特性

寫程式時的「具體 → 寬泛」走法保留了決策軌跡;「寬泛 → 具體」走法丟失了軌跡。


三類範圍的共同骨架

「最小必要範圍」原則跨三類獨立議題:

議題範圍對象失敗模式對應實作篇
JS 元件邊界「我可以動什麼」的契約越界操作 framework 管的部分、被重繪清掉#13 元件邊界與 JS 操作的影響範圍
Selector 精準度「query 命中哪些元素」誤命中、未來結構變動就壞#14 Selector 精準度
Observer 範圍「監聽哪些變動」過度觸發、layout 抖動、無限循環#29 MutationObserver 範圍與觸發頻率

三者表現不同、機制不同、但底層都是同一條原則的應用 — 越寬越脆弱、越具體越穩定。

共通的設計工具

跨三類議題、設計「最小必要範圍」的工具有共通模式:

維度三類議題的對應
起點 / 邊界JS:元件邊界契約;Selector:query 起點;Observer:root
深度JS:操作層級;Selector:是否找深層;Observer:subtree
過濾JS:操作前界定;Selector:attribute filter / :not;Observer:option flag

每個議題都有「起點 / 深度 / 過濾」三維度可顯式設計 — 同樣的設計骨架在不同情境重現。


應用辨識訊號

下次工作中、看到這些訊號該想到「最小必要範圍」:

訊號對應議題行動
「我先寫寬一點、之後有問題再縮」任一類反向、第一版就寫具體
「現在只一個元件、document.query 也行」Selector 起點用元件根變數、預防未來擴展
「subtree: true 比較保險」Observer 範圍縮到實際關心的子節點
「先 framework 內注入個 element 看看」JS 元件邊界留在 framework 邊界外
「同樣的 bug 出現在不同元件」任一類範圍寬了、影響跨越元件邊界
「改了 X、Y 跟 Z 也壞」任一類範圍寬了、改動波及

不該套用「最小必要範圍」的情境

這條原則有適用邊界、不是所有「縮」都有意義:

情境為什麼不套用
探索 / debug 階段寬範圍幫助觀察全貌、確認問題範圍後再縮
一次性 script、跑完就丟沒有「未來變動」的問題、簡潔優先
確實需要看深層變動的 observersubtree: true 是必要、不是 over-broad
確實要對全頁套用的操作(theme 切換)全頁面才是「最小必要範圍」

核心判準:「最小必要範圍 = 滿足當前需求的最窄範圍」 — 不是極致最小、是「不再小就會漏」的點。盲目縮到「比需要還小」會犧牲覆蓋率、是另一種錯誤。


跟「2 次門檻」的協同

#42 2 次門檻 處理「失敗第 N 次該換策略」、本原則處理「第一次設計時範圍該多大」。兩者方向互補:

  • 2 次門檻:失敗發生後、何時該升級處理層級
  • 最小必要範圍:寫第一版時、就該追求 sanity 防線

如果第一版就遵循「最小必要範圍」、後續觸發 2 次門檻的機率會降低 — sanity 防線是預防、2 次門檻是補救。


對應的實作篇

每篇示範這個原則在不同議題的應用:

議題範圍對象
#13 元件邊界與 JS 操作的影響範圍JS 元件邊界「我可以動什麼」契約
#14 Selector 精準度DOM query 範圍起點 / 範圍 / 過濾三維度
#29 MutationObserver 範圍與觸發頻率Observer 監聽範圍root / option / 頻率三維度

讀的時候從本篇出發、依議題挑實作篇。


判讀徵兆

訊號自問回應
「之後有問題再縮」縮的時候會知道哪些是故意寬嗎?否 → 第一版就寫具體
「以防萬一勾 subtree」真的有深層變動需要監聽嗎?否 → 移除 subtree
「document.query 比較簡單」未來頁面會不會有第二個同名元素?不確定 → 用元件根變數
「怕 selector 太窄漏掉」漏掉時會怎樣、可以擴大嗎?可以 → 從具體開始、漏了再擴
Bug 表現「不知道哪改的」範圍是否寬了、波及不該動的地方?是 → 縮範圍

核心原則:最小必要範圍守護的是「行為可預測」 — 寫的時候多想一點、debug 時少痛一點。寬範圍的代價不是慢、是出錯時定位困難 — 這也是為什麼這條原則在「沒效能瓶頸」的情境下仍然成立。

延伸到 stream 操作:#64 Feature 操作要跟 Source 同層合成 是本原則在 stream 領域的應用 — 「合成位置」就是「操作的範圍邊界」、選錯位置 = 範圍錯。寬範圍便利、窄範圍對齊、兩者反相關的更高層原則見 #67 寫作便利度跟意圖對齊反相關