
最近在看一些適合用 React Context 情境範例時,看到可以應用在 Breakpoint 上,意外發現過去幾年取得斷點資料的寫法,竟然讓應用程式中重複起了這麼多個 resize 監聽器 😨。
原本寫了一個 useBreakpoints Hook,內部邏輯很單純:
在內部監聽 window 的 resize 事件,然後計算出 isMobile, isDesktop 等狀態後回傳。
一直覺得這 Hook 很好用,在很多個元件裡都直接呼叫了:
const { isMobile, isDesktop } = useBreakpoints();
踩坑點來了!!
隨著應用程式規模擴大,元件數量增加:
N 個元件呼叫 Hook,就啟動了 N 個獨立的
resize事件監聽器!
每一個監聽器都必須獨立執行 Debounce 邏輯、計算新狀態、然後觸發自己的元件重新渲染。
假設我的電商頁面可能有 30 個元件在判斷斷點,這等於瀏覽器要處理 30 組事件、30 次狀態更新。
這就是「N 個監聽器地獄」,對使用者的設備造成了不必要且巨大的資源浪費。
斷點資訊本質上是一種「全域狀態」,它應該被統一管理,而不是分散在每個元件的獨立 Hook 裡,改採用 Context + Provider 模式。
將狀態提升至頂層 Provider,僅註冊單一監聽器,子元件用 useContext 訂閱斷點狀態。
設計要點:
BreakpointProvider 成為唯一負責監聽和計算狀態的「中央資訊站」。window.addEventListener('resize', ...) 邏輯集中放在 Provider 的 useEffect 中,確保整個應用程式中只有一個計算斷點邏輯的 resize 監聽器在工作。useContext(BreakpointContext) 訂閱由 Provider 廣播出來的單一狀態。
這徹底解決了 N 個監聽器的問題。
進一步優化:
為了應對 Context 的「全域重新渲染」風險,在 Provider 內部再增加兩層防護:
條件式更新:
在 handleResize 內嚴格判斷,只有當裝置從 Mobile 跨到 Desktop 時,才更新狀態。
記憶化:
用 useMemo 包裹 Context 的 value。
這樣,當視窗寬度變動但斷點狀態不變時,value 的參考地址(Referential Equality)就不會變。如此一來,Context 的子元件都不會被通知重新渲染。
網站的監聽器、事件處理次數大幅減少,減輕了 CPU 負載,狀態更新次數也大幅下降,提升了整體網頁效能!!🎉