ResizeObserver loop 錯誤怎麼修?先找尺寸回授,不要只隱藏訊息
本文目錄
圖表、編輯器或響應式元件初始化後,錯誤監控出現 ResizeObserver loop completed with undelivered notifications.,畫面卻還能使用。這時先檢查 callback 有沒有再次改動被觀察元素的尺寸,以及這些變動最後是否會收斂。
停止無意義的尺寸寫入、避免觀察與寫入互相影響,才是修正回授的起點。 requestAnimationFrame() 可以把工作移到下一個 frame,但無法把「每次都加 10px」的無限變化變成穩定版面。
本文依 MDN 與 Resize Observer 規格草案整理。尺寸序列對照已在 Chrome 執行。後面的固定目標控制器供測試頁參考,其中尺寸計算函式已用 Node.js 驗證。
這個錯誤表示部分通知被延後
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,就少一條讀取和寫入的回授路徑。
用兩個尺寸序列辨認 rAF 有沒有消除回授
把下面存成獨立的 HTML 頁面,在瀏覽器打開。兩輪都等到下一個 frame 才寫入。第一輪以本次讀到的寬度加 10px,第二輪固定寫到 120px。第一輪達到三次觀察就主動清理,重現過程會自行停止。
<!doctype html><meta charset="utf-8"><title>ResizeObserver 尺寸回授檢查</title><div id="probe" style="width: 100px; height: 20px"></div><pre id="output"></pre><script type="module"> const probe = document.querySelector('#probe'); const output = document.querySelector('#output');
function sample(nextWidth) { probe.style.width = '100px'; return new Promise(resolve => { const widths = []; let frame; const observer = new ResizeObserver(([entry]) => { const width = entry.contentRect.width; widths.push(width); const next = nextWidth(width); if (widths.length === 3 || next === width) { observer.disconnect(); cancelAnimationFrame(frame); resolve(widths); return; } frame = requestAnimationFrame(() => { probe.style.width = `${next}px`; }); }); observer.observe(probe); }); }
const growing = await sample(width => width + 10); const stable = await sample(() => 120); output.textContent = JSON.stringify({ growing, stable });</script>在 Chrome 157 的本機測試頁,第一輪讀到 100 → 110 → 120,第二輪讀到 100 → 120 並停止寫入。兩輪都使用 rAF,差別在下一個寬度是否取決於剛量到的值。正式頁可先記錄元素識別、entry.contentRect.width 和即將寫入的目標值。若每次寫入都造成下一次不同的讀值,就沿著改尺寸的程式追查。這個對照檢查尺寸是否收斂,沒有刻意觸發 Console 的 loop 訊息。
必須寫尺寸時,用不依賴本次量測的目標
以下範例把目標尺寸交給外部控制,觀察通知只用來確認是否需要套用。它以水平書寫模式的 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