ResizeObserver loop 錯誤怎麼修?先找尺寸回授,不要只隱藏訊息
圖表、編輯器或響應式元件初始化後,錯誤監控出現 ResizeObserver loop completed with undelivered notifications.,畫面卻還能使用。這時先檢查 callback 有沒有再次改動被觀察元素的尺寸,以及這些變動最後是否會收斂。
停止無意義的尺寸寫入、避免觀察與寫入互相影響,才是修正回授的起點。 requestAnimationFrame() 可以把工作移到下一個 frame,但無法把「每次都加 10px」的無限變化變成穩定版面。
本文依 MDN 與 Resize Observer 規格草案整理。下方 DOM 程式是可自行放進測試頁的示例,尚未在瀏覽器執行;本機只驗證尺寸計算函式,不把這個結果當成瀏覽器錯誤已消失的證據。
這個錯誤表示部分通知被延後
ResizeObserver 在畫面繪製前通知尺寸變化。callback 如果改變 layout,可能再產生新通知;瀏覽器限制同一輪可以繼續處理的觀察項目,未送出的通知會延到後續 frame,並產生這個 error event。
MDN 的 Observation Errors 說明指出,這個機制避免瀏覽器被迴圈鎖住,卻不會修好應用程式裡的無限回授。一開始出現一次後穩定,與每幀持續出現、元素不斷變大的問題,應分開判斷。
先留下觸發操作、元件名稱與尺寸序列。如果圖表只在首次渲染調整兩次,不要直接宣稱它必然洩漏或無限迴圈;如果寬度一直增加,就要追到具體的尺寸寫入。
第一個要找的是「量到多少,再加多少」
以下寫法每次收到寬度,就把同一元素改得更寬:
// 反例:不要放進正式頁面。const observer = new ResizeObserver(([entry]) => { entry.target.style.width = `${entry.contentRect.width + 10}px`;});observer.observe(document.querySelector('.panel'));假設依序量到 100、110、120,目標也就持續變成 110、120、130。加 debounce 或 requestAnimationFrame 只會降低變化速度,沒有消除依賴。
回到產品需求:這個元件到底要固定寬度、跟隨父層,還是只有內部畫布需要重新繪製?能用 CSS 達成的尺寸關係就交給 CSS;需要 JavaScript 時,優先觀察容器,更新不反過來撐大容器的內容。
如果只是依容器寬度切換卡片排列,可評估 CSS container queries。少一個 callback,就少一條讀取和寫入的回授路徑。
必須寫尺寸時,用不依賴本次量測的目標
以下範例把目標尺寸交給外部控制,觀察通知只用來確認是否需要套用。它以水平書寫模式的 HTML 元素與 content box 為前提,CSS 同時設定 box-sizing: content-box,避免量到的內容寬度與寫入的盒模型混用:
<div id="panel" style="box-sizing: content-box; width: 160px; height: 80px; padding: 0; border: 0; background: lightblue"></div>function normalizeWidth(value) { if (!Number.isFinite(value) || value < 0) { throw new RangeError('width must be finite and non-negative'); } return Math.round(value);}
function attachWidthTarget(element, initialWidth) { let targetWidth = normalizeWidth(initialWidth); let pendingFrame = null; let disposed = false;
function schedule() { if (disposed || pendingFrame !== null) return; pendingFrame = requestAnimationFrame(() => { pendingFrame = null; if (disposed) return; if (element.style.width !== `${targetWidth}px`) { element.style.width = `${targetWidth}px`; } }); }
const observer = new ResizeObserver(([entry]) => { if (Math.abs(entry.contentRect.width - targetWidth) > 0.5) schedule(); }); observer.observe(element); schedule();
return { setWidth(value) { if (disposed) return; targetWidth = normalizeWidth(value); schedule(); }, dispose() { disposed = true; observer.disconnect(); if (pendingFrame !== null) cancelAnimationFrame(pendingFrame); pendingFrame = null; }, };}
const controller = attachWidthTarget(document.querySelector('#panel'), 240);// UI 有新的目標時呼叫 controller.setWidth(320)。// 元件卸載時呼叫 controller.dispose()。0.5px 是這個示例用來容忍次像素差異的策略,不是規格要求。更重要的是目標值固定:套用 240px 後,不會因為下一次量到 240px 又決定加寬。如果父層 max-width 讓目標無法達成,範例也不反覆寫入同一個 inline width;此時應檢查 CSS 約束,而不是讓 observer 與 CSS 搶控制權。
這份示例展示回授與清理的處理方式。單純設定固定寬度其實不需要 observer;真實圖表或 canvas 元件應把需要觀察的容器與需要更新的內容分開,不能把範例直接當成所有元件的通用修補。
normalizeWidth 已在 Node.js v24.13.0 檢查整數化、零寬、負數與 NaN。這只驗證目標輸入,不涵蓋 layout、繪製時序或瀏覽器 observer 行為。
卸載時,observer 和排程要一起清理
disconnect() 停止此 observer 的全部觀察;只停止某一個元素時用 unobserve(element)。但已排好的 requestAnimationFrame 不會因為 disconnect 自動取消,所以要另外保留 frame ID 並清除。
範例的 disposed 也能讓已被保留的 callback 不再寫入。React、Vue 或 Svelte 的整合請接到各自的卸載生命週期,不要只在元件被建立時 observe,卻在多次切換路由後留下舊實例。
站內 CSS 倒圓角 Tab Bar採取另一個方向:觀察元件尺寸後只更新凹槽 transform,沒有改被觀察目標的寬高。這種量測與位置更新的分工,適合不需要改變 layout 尺寸的互動。
用這些條件驗收,而不是只看 Console 安靜
- 縮放父容器、切換側欄和更新文字,記錄 callback 次數及尺寸是否停止變化。
- 若用了 rAF,確認沒有每幀持續寫相同值或持續增加的寬度。
- 卸載後再改父層尺寸,確認沒有舊 callback 操作已移除的元素。
- 測窄螢幕、字型載入與有 padding/border 的版本;盒模型變更後要重新檢查量測值。
若來源是第三方圖表或編輯器,先縮成只含該元件的重現頁,查對應版本的 issue 或更新紀錄;看到相同錯誤文字只能證明症狀相近,不能直接認定根因相同。
錯誤監控可以另行分類已確認的單次初始化通知,但先確定畫面會收斂且沒有破版。把所有 ResizeObserver 錯誤過濾掉,只會讓下一次真正的迴圈更難被發現。
參考資料:
MDN:ResizeObserver 與 Observation Errors
CSSWG:Resize Observer Module Level 1 規格草案
MDN:ResizeObserverEntry.contentRect