題幹與適用場景
你維護一個多欄儀表板。卡片尺寸由網格、拖曳、側欄和字型共同決定,不能假設它只隨視口變化。卡片寬度低於 320px 時顯示緊湊標題和單欄指標,高於該閾值時顯示完整布局;視窗不變時,側欄收合也必須立即生效。
這道題考察元素級尺寸觀察、瀏覽器布局時序、React 生命週期和效能邊界。公開前端面試資料通常涵蓋 DOM、CSS、瀏覽器事件和效能;官方文件則明確說明 ResizeObserver 的通知語意與迴圈保護,適合把「會用 API」追問到可驗證的工程設計。
面試官考察點
- 能否說明
window.resize只描述視口變化,無法涵蓋網格重排、字型替換或父容器改變。 - 能否選擇
ResizeObserver的content-box或border-box,並解釋尺寸含義。 - 能否把測量寫入狀態或 CSS 變數,而不是在回呼同步改尺寸造成反饋迴圈。
- 能否處理觀察目標變化、元件卸載、隱藏元素、舊瀏覽器降級和大量卡片成本。
- 能否用真實使用者指標、長任務和布局軌跡證明流暢,而非只看回呼次數。
回答前需要釐清的問題
- 閾值依據內容寬度、邊框盒寬度,還是 CSS 容器查詢已能表達的純樣式差異?
- 尺寸變化是否要觸發資料請求,還是只改變呈現?
- 需要連續跟隨每一幀,還是允許一幀內合併一次更新?
- 最低瀏覽器版本是否支援 ResizeObserver?伺服器渲染階段是否可能存取
window? - 卡片數量是幾十個還是上千個?是否有虛擬列表或只觀察可見卡片?
30 秒回答框架
「我先確認這是元素尺寸而非視口尺寸問題。純樣式切換優先用 CSS 容器查詢;需要把尺寸交給 JavaScript 時,在客戶端為卡片掛載 ResizeObserver,讀取約定的盒子尺寸,經過閾值和去重後寫入 CSS 變數或狀態。回呼只讀測量,不同步修改會影響被觀察尺寸的樣式;必要的 DOM 寫入放到下一幀,並在清理時 unobserve。大量卡片要限制觀察範圍並測量 INP、長任務和布局成本。舊環境用視窗監聽或固定布局作為明確降級。」
分步深入解答
第一步:先判斷 CSS 是否足夠
若只是「寬度小於 320px 顯示緊湊樣式」,容器查詢通常比 JavaScript 簡單,也不會引入測量狀態。只有尺寸要驅動圖表採樣、虛擬化視窗、第三方渲染器或可觀測業務邏輯時,才需要 ResizeObserver。
第二步:定義尺寸契約
ResizeObserverEntry 可提供 content box、border box 等尺寸。先約定閾值對應哪個盒子,統一單位與取整策略。不要把一次浮點測量直接當作穩定業務事件;寬度在 319.9 和 320.1 之間變化時,可按整數或斷點狀態去重。
第三步:建立觀察生命週期
客戶端元件掛載後建立 observer,觀察卡片節點;節點替換或元件卸載時呼叫 unobserve 或 disconnect。React 中讓 ref 負責目標節點,Effect 負責 observer 的建立與清理,避免每次普通渲染都重建實例。
const ref = useRef<HTMLDivElement>(null)
const [compact, setCompact] = useState(false)
useEffect(() => {
const node = ref.current
if (!node || !('ResizeObserver' in window)) return
const observer = new ResizeObserver(([entry]) => {
const width = entry.contentRect.width
const next = width < 320
setCompact((current) => (current === next ? current : next))
})
observer.observe(node)
return () => observer.disconnect()
}, [])第四步:避免觀察回呼形成反饋迴圈
如果回呼修改被觀察元素寬高,修改又觸發下一輪通知,可能出現 ResizeObserver loop completed with undelivered notifications。回呼只更新尺寸契約相關的離散狀態,或把非必要視覺寫入安排到 requestAnimationFrame,並用預期尺寸或狀態去重。
第五步:分離測量和渲染
測量值可寫入 CSS 自訂屬性,讓 CSS 負責布局;也可只保留 compact 這類離散狀態。避免每個像素變化都塞進 React state。若需要連續值,使用 rAF 合併同一幀通知,並記錄掉幀與長任務。
第六步:處理多卡片和不可見節點
大量卡片應只觀察可見節點,或由布局層統一分發尺寸。display: none、收合面板和虛擬列表會改變可觀測尺寸,恢復可見後要驗證首個通知能重新建立狀態。
第七步:明確降級與伺服器邊界
伺服器渲染不能存取 window。元件應在客戶端 Effect 中檢測能力;不支援 ResizeObserver 時使用固定布局、CSS 媒體查詢或節流視窗監聽,並說明降級失去哪些元素級適配。
第八步:用證據驗證體驗
測試側欄收合、拖曳、字型載入、網格重排、螢幕旋轉和瀏覽器縮放。Chrome Performance 記錄回呼、布局、繪製和長任務;用 INP 或輸入延遲確認拖曳仍能回應。專門斷言控制台無循環告警,並確認卸載後 observer 不再更新狀態。
設計取捨與邊界
容器查詢與 ResizeObserver
容器查詢適合純 CSS 斷點;ResizeObserver 適合 JavaScript 計算或第三方渲染。兩者可並存,邊界由尺寸是否要離開樣式層決定。
content-box 與 border-box
content-box 適合內容排版閾值,border-box 適合包含邊框和內距的外框。選錯盒子會造成斷點提前或延後;答案應把選擇寫進契約和測試。
連續值與離散值
連續寬度能驅動圖表,但更新頻率高;離散斷點更穩定、易測試。先用離散狀態滿足需求,只有效能預算和視覺收益都明確時才引入連續更新。
失敗演練與演進計畫
失敗:只監聽 window.resize
側欄收合和網格重排不會觸發窗口事件,卡片會停留在錯誤布局。修復是觀察卡片或其容器,並補充非窗口變化測試。
失敗:回呼直接修改被觀察尺寸
尺寸變化觸發回呼,回呼再次改變尺寸,可能形成迴圈和額外布局。修復是寫入獨立 CSS 變數、使用離散狀態,或在下一幀更新且保證冪等。
失敗:每個像素變化都 setState
拖曳時會產生高頻 React 渲染,輸入回應變差。修復是閾值去重、rAF 合併、只觀察可見卡片,並用 Performance 面板確認布局成本。
常見誤區與追問
誤區:ResizeObserver 是更強的 resize 事件
它觀察元素盒子並按瀏覽器布局時序投遞通知,不是窗口事件的簡單替代。
追問:如何在 React Strict Mode 下驗證?
確認開發環境建立與清理成對出現,observer 數量不會累積;不要把 Strict Mode 額外檢查誤判為生產重複訂閱。
追問:能否在回呼呼叫 getBoundingClientRect?
可以讀取,但要意識到額外測量可能增加布局成本。優先使用 entry 提供的盒子尺寸,並用效能軌跡證明必要性。
追問:如何測試尺寸迴圈?
讓回呼改變會影響自身尺寸的樣式,觀察控制台告警、通知次數和最終尺寸;修復後斷言狀態穩定且沒有持續通知。
追問:什麼時候完全不需要 JavaScript?
只要需求是容器斷點下的樣式變化,就優先 CSS 容器查詢;腳本留給資料、測量或第三方渲染真正需要的場景。
追問:如何證明優化有效?
比較同一拖曳腳本下的 INP、長任務數量、布局耗時、React 提交次數和記憶體;不要只比較 observer 回呼計數。