“ResizeObserver Loop Completed”错误是什么意思
了解ResizeObserver循环错误及浏览器延迟通知的原因,识别真实布局循环,并在错误追踪工具和Cypress中安全过滤两种错误消息。
“ResizeObserver loop completed with undelivered notifications”是浏览器派发的错误事件,并不是你的代码抛出的异常。它表示部分尺寸变化通知没能在当前帧内送达,被推迟到了下一帧。大多数情况下,可以放心忽略它。
这个错误常常以数百条事件的形式出现在错误追踪工具中,而且没有任何有用的调用栈;它也可能导致 Cypress 测试运行失败。而很多时候,你自己的代码里根本没有用到 ResizeObserver。本文将解释浏览器在这一过程中具体做了什么,给出一个最小复现示例,并说明如何区分无害噪音与真正的循环。文末还提供了一个能同时匹配新旧两种错误消息的过滤规则。
核心要点
- 当尺寸变化通知不得不等到下一帧才能送达时,浏览器会在
window上派发“ResizeObserver loop completed with undelivered notifications.”错误事件。 - 旧版浏览器针对同一情况报告的消息是“ResizeObserver loop limit exceeded”,因此任何过滤规则或测试处理函数都需要同时匹配这两种消息。
- 这一保护机制只能防止浏览器卡死,并不能终止循环:如果回调持续改变其所观察元素的尺寸,该错误会在每一帧都被触发。
try/catch永远捕获不到这个错误,但window.onerror、错误追踪 SDK 以及 Cypress 的uncaught:exception处理函数都能捕获到。- 应在错误追踪工具中过滤该错误,而不是在全局
window.onerror中直接吞掉它,同时持续关注其触发频率。
“ResizeObserver Loop Completed With Undelivered Notifications”是什么意思?
“ResizeObserver loop completed with undelivered notifications”这条消息表示,浏览器有一些尺寸变化通知无法在一帧之内送达,因此将其推迟处理。如果你需要回顾该 API 本身,请参阅 ResizeObserver vs window resize。
浏览器在布局(layout)之后、绘制(paint)之前处理尺寸观察。如果某个回调改变了布局,浏览器会重新计算布局并再次执行回调。每一轮只会处理那些在 DOM 中比上一轮所处理的最浅层元素更深的元素。不满足这一规则的通知将等到下一帧处理,同时浏览器会报告该错误。MDN 的 Observation Errors 章节描述了这一流程,而 Resize Observer 规范则定义了上述深度规则。
你会看到以下两种消息之一:
ResizeObserver loop completed with undelivered notifications.(当前的措辞,末尾带句点)ResizeObserver loop limit exceeded(旧版浏览器使用的措辞)
最小复现示例
下面这个文件可以复现两种行为。其中的回调会修改它正在观察的元素的宽度。
<div id="box" style="width:100px;height:40px;background:tomato"></div>
<script>
const box = document.getElementById('box');
const LOOP = false; // set to true for the endless case
window.addEventListener('error', (e) => console.log('window error:', e.message));
const ro = new ResizeObserver(([entry]) => {
const width = entry.contentBoxSize[0].inlineSize;
entry.target.style.width = LOOP ? `${width + 10}px` : '200px';
});
ro.observe(box);
</script>
当 LOOP = false 时,第一次回调将方块的宽度改为 200px。这次尺寸变化发生在同一个元素上,因此它并不比刚处理过的元素更深。浏览器将其推迟,并记录一次错误。到了下一帧,回调再次写入 200px,尺寸没有发生变化,观察器也就不再触发。
当 LOOP = true 时,每一帧都会让方块变得更宽,错误也会在每一帧都被记录。
为什么 try/catch 捕获不到 ResizeObserver 错误?
try/catch 永远捕获不到 ResizeObserver 循环错误,因为你的回调根本没有抛出任何异常,它是正常执行完毕的。浏览器随后会在 window 上派发一个 error 事件,这发生在你所能控制的任何调用栈之外。
try {
ro.observe(box);
} catch (err) {
console.log('never reached'); // the error is dispatched on window later
}
window.onerror 和 error 监听器能够捕获 window 级别的错误事件,而错误追踪 SDK 也挂载在同样的事件上,所以追踪工具会记录下这个错误。由于该事件并不携带带有调用栈的 Error 对象,追踪工具通常无法显示有用的调用栈。
ResizeObserver 错误什么时候才是真正的问题?
当布局无法趋于稳定时,ResizeObserver 循环错误才值得关注。MDN 指出,这一保护机制可以防止浏览器卡死,但无法阻止循环本身。即使是最终能够稳定下来的循环,由于变化被分散到多个帧中,也可能会短暂地显示出中间状态的布局。
| 你观察到的现象 | 含义 | 应对措施 |
|---|---|---|
| 页面加载或布局变化后出现少量事件,页面看起来稳定 | 循环在一两帧内就稳定了 | 过滤掉即可 |
| 与某个组件相关的突发事件,且伴随可见的跳动 | 某个组件正在改变其所观察盒子的尺寸 | 修复问题或向上游报告 |
| 持续不断的事件流、不断增大的元素或 CPU 占用持续上升 | 循环永远无法稳定 | 立即修复 |
如何修复 ResizeObserver 循环?
修复 ResizeObserver 循环的方法是:阻止回调修改其所观察元素的尺寸,或者将这次写入操作移出当前帧。请按顺序尝试以下方案。
- 不要在元素自身的回调中修改被观察元素的尺寸。 改为写入另一个元素,且该元素的尺寸不会反过来影响观察结果。如果只是为了实现视觉上的响应式效果,CSS 容器查询(container queries)可以完全避免使用 JavaScript。
- 使用
requestAnimationFrame延迟执行。 将写入操作移到requestAnimationFrame中,它会在下一帧执行,错误也就不再出现。当你确实需要持续调整尺寸时,MDN 推荐采用这种方式。在LOOP的场景下,方块仍然会无限增大,消失的只是错误本身。 - 跳过由你自己设置的尺寸。 对于非预期的循环,MDN 建议使用
WeakMap记录每个元素的预期尺寸:
const expected = new WeakMap();
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentBoxSize[0].inlineSize;
if (expected.get(entry.target) === width) continue; // our own change
requestAnimationFrame(() => {
const next = width + 10;
entry.target.style.width = `${next}px`;
expected.set(entry.target, next);
});
}
});
由于状态是按元素分别存储的,同时观察多个元素也是安全的。当回调发现宽度正是它自己设置的值时,就会跳过该条目,循环随之结束。
UI 库同样会触发该错误
最常见的触发原因是某个 ResizeObserver 回调改变了其所观察元素的尺寸。而这样的回调往往存在于组件库中,而非你自己的代码里。那些先测量容器、再调整自身或子元素尺寸的组件是典型来源,例如自适应高度的文本框、虚拟滚动表格、图表和弹出层(popover)。如果错误在某个特定组件挂载或打开时出现,请升级该库,或向上游提交一个复现示例。
如何安全地过滤该错误?
使用同一个正则表达式匹配两种消息,并将其应用于错误追踪工具和测试运行器中,而不是全局处理函数中。
const RO_LOOP_ERROR =
/ResizeObserver loop (completed with undelivered notifications|limit exceeded)/;
// cypress/support/e2e.js
Cypress.on('uncaught:exception', (err) => {
if (RO_LOOP_ERROR.test(err.message)) return false;
});
每当你的应用抛出未被捕获的错误时,Cypress 都会触发 uncaught:exception;处理函数返回 false 即可告诉 Cypress 不要让测试失败。Cypress 事件目录(Catalog of Events)对此有详细说明。在 Sentry 中,可添加 ignoreErrors: [RO_LOOP_ERROR]。根据 Sentry 配置项参考文档,字符串条目按子串匹配,正则表达式条目则按原样进行测试。较新版本的 Sentry JavaScript SDK 已默认忽略新版措辞,但不包括旧版措辞,因此只有在需要匹配旧版字符串或使用较旧的 SDK 时,才需要添加自定义条目。
不要在 window.onerror 中吞掉这个错误。一刀切的屏蔽会把无限循环的情况也一并隐藏,你还会因此失去频率这一信号。正如这篇错误日志最佳实践所述,在错误追踪工具层面进行过滤,可以让这一决策保持透明且易于撤销。之后再持续关注错误数量:布局变化后偶尔出现的突发事件属于噪音;而来自同一会话的持续事件流,则表明存在一个永远无法稳定的循环。会话回放(session replay)能够显示用户在错误触发的那一刻是否看到了布局跳动,从而帮助你判断它是否只是噪音。同样的排查思路也适用于其他生产环境中的高频噪音错误,例如“Failed to Fetch Dynamically Imported Module”。
总结
在大多数情况下,“ResizeObserver loop completed”错误只是意味着浏览器推迟了部分通知,而布局在一帧之后就稳定了下来。请检查布局是否能够稳定,以及是否存在明显的跳动。如果两个问题的答案都令人满意,就在错误追踪工具中过滤掉这两种消息。否则,请找出那个修改其所观察元素尺寸的回调,并通过延迟执行或预期尺寸校验来修复它。
常见问题
setTimeout 或防抖(debounce)是比 requestAnimationFrame 更好的修复方案吗?
不是。两者都能将尺寸写入操作移出当前帧,从而让错误消失,但都无法阻止持续增大元素的循环。setTimeout 回调可能在浏览器完成绘制之后才执行,因此用户可能会先看到一帧旧布局,然后变化才生效。requestAnimationFrame 则在下一次绘制之前执行,能让更新与渲染保持同步。
为什么错误会在组件挂载后立即出现?
当对一个已渲染且尺寸不为零的元素调用 observe() 时,ResizeObserver 会发送首次通知。如果回调在这次首次通知中为该元素写入了新的尺寸,浏览器就会推迟后续通知,并立即报告该错误。那些在挂载时测量并调整自身尺寸的组件正是以这种方式触发错误的,即使页面上并没有任何元素发生尺寸变化。
CSS 容器查询能否取代 ResizeObserver 回调?
可以,前提是回调只是根据容器宽度来修改样式。容器尺寸查询会应用尺寸包含(size containment),因此容器的尺寸不会取决于其内容,样式也就不会反过来影响测量结果。尺寸查询在 Chrome 105、Safari 16、Firefox 110 及更高版本中可用。对于 CSS 无法完成的工作,例如重新计算图表尺寸或虚拟滚动的行数,仍应保留 ResizeObserver。
当调用栈为空时,如何找出是哪段代码触发了该错误?
在开发环境中,临时用一个子类替换 window.ResizeObserver,让它记录每个被观察的元素并包装回调。然后复现该错误,检查在 window 错误事件触发之前是哪个元素的尺寸发生了变化。如果回调位于第三方库内部,可以逐个移除组件,直到错误不再出现,以此缩小排查范围。