1510 字
8 分鐘

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 安靜#

  1. 縮放父容器、切換側欄和更新文字,記錄 callback 次數及尺寸是否停止變化。
  2. 若用了 rAF,確認沒有每幀持續寫相同值或持續增加的寬度。
  3. 卸載後再改父層尺寸,確認沒有舊 callback 操作已移除的元素。
  4. 測窄螢幕、字型載入與有 padding/border 的版本;盒模型變更後要重新檢查量測值。

若來源是第三方圖表或編輯器,先縮成只含該元件的重現頁,查對應版本的 issue 或更新紀錄;看到相同錯誤文字只能證明症狀相近,不能直接認定根因相同。

錯誤監控可以另行分類已確認的單次初始化通知,但先確定畫面會收斂且沒有破版。把所有 ResizeObserver 錯誤過濾掉,只會讓下一次真正的迴圈更難被發現。

參考資料:

MDN:ResizeObserver 與 Observation Errors

CSSWG:Resize Observer Module Level 1 規格草案

MDN:ResizeObserverEntry.contentRect

MDN:ResizeObserver.unobserve()

MDN:CSS container queries

ResizeObserver loop 錯誤怎麼修?先找尺寸回授,不要只隱藏訊息
https://laplusda.com/posts/resizeobserver-loop-error-fix/
作者
Zero
發佈於
2026-10-03
許可協議
CC BY-NC-SA 4.0