「ResizeObserver Loop Completed」エラーの意味とは
ResizeObserverのループエラーと通知が遅れる理由を解説。実際のレイアウトループを見分け、2種類のエラーメッセージを安全に除外する方法を紹介します。
「ResizeObserver loop completed with undelivered notifications」は、ブラウザが発生させるエラーイベントであり、自分のコードから投げられた例外ではありません。一部のリサイズ通知が現在のフレーム内で配信されず、次のフレームに持ち越されたことを意味します。ほとんどの場合、無視して問題ありません。
このエラーは、エラートラッカー上で有用なスタックのない数百件のイベントとして現れたり、Cypress の実行を失敗させたりします。しかも、自分のコードのどこにも ResizeObserver が使われていないことも珍しくありません。本記事では、ブラウザの内部で何が起きているかを解説し、最小限の再現コードを示したうえで、無視してよいノイズと本当のループを見分ける方法を説明します。最後に、両方のバージョンのメッセージにマッチするフィルターを紹介します。
要点
- リサイズ通知を次のフレームまで待たせる必要がある場合、ブラウザは「ResizeObserver loop completed with undelivered notifications.」を
window上のエラーイベントとして発生させます。 - 古いバージョンのブラウザは、同じ状況を「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」というメッセージは、ブラウザが 1 フレーム内で配信しきれなかったリサイズ通知を抱えており、それらを後回しにしたことを意味します。API 自体の復習が必要な場合は、ResizeObserver vs window resize を参照してください。
ブラウザは、レイアウトの後、ペイントの前にリサイズの監視処理を行います。コールバックがレイアウトを変更すると、ブラウザは再計算を行い、再びコールバックを実行します。各パスでは、前のパスで処理された最も浅い要素よりも 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 にリサイズします。このリサイズは同じ要素に対するものなので、直前に処理された要素より深い位置にはありません。そのためブラウザはこれを後回しにし、エラーを 1 回だけ記録します。次のフレームでコールバックは再び 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 が指摘しているとおり、この安全機構はブラウザのフリーズを防ぎますが、ループそのものは防ぎません。最終的に安定するループであっても、変更が複数フレームにまたがる間、中間状態のレイアウトが一瞬表示されることがあります。
| 観察される現象 | 意味 | 対応 |
|---|---|---|
| 読み込み後やレイアウト変更後に数件発生し、ページは安定して見える | ループが 1〜2 フレーム以内に収束した | フィルタリングする |
| 特定のコンポーネントに紐づいて断続的に発生し、目に見えるガタつきがある | コンポーネントが自身の監視対象のボックスをリサイズしている | 修正するか、上流に報告する |
| 途切れなく発生し続ける、要素が拡大し続ける、または CPU 使用率が上昇する | 収束しないループ | 直ちに修正する |
ResizeObserver のループはどう修正するか
ResizeObserver のループを修正するには、コールバックが監視対象の要素をリサイズしないようにするか、その書き込みを現在のフレームの外に移します。以下の選択肢を順に検討してください。
- 監視対象の要素を、そのコールバック内でリサイズしない。 サイズが監視に影響を及ぼさない別の要素に書き込みます。純粋に見た目のレスポンシブ対応であれば、CSS コンテナクエリを使えば 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 のコールバックです。そしてそのコールバックは、自分のコードではなくコンポーネントライブラリの中にあることがよくあります。コンテナのサイズを計測してから自身や子要素をリサイズするコンポーネントが典型的な発生源で、たとえば自動サイズ調整のテキストエリア、仮想化テーブル、チャート、ポップオーバーなどが挙げられます。特定のコンポーネントのマウント時やオープン時にエラーが発生する場合は、ライブラリをアップデートするか、再現手順を添えて上流に報告してください。
エラーを安全にフィルタリングするには
1 つのパターンで両方のメッセージにマッチさせ、グローバルハンドラーではなく、トラッカーとテストランナーに適用します。
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 でエラーを握りつぶしてはいけません。一律に抑制すると無限ループのケースまで隠れてしまい、発生頻度というシグナルも失われます。こちらのエラーロギングのベストプラクティスで説明しているように、トラッカーのレベルでフィルタリングすれば、その判断が可視化され、容易に元に戻せます。そのうえで件数を監視しましょう。レイアウト変更後にたまに発生するバーストはノイズです。1 つのセッションから途切れなく発生している場合は、収束しないループを示しています。セッションリプレイを使えば、エラー発生時にユーザーがレイアウトのガタつきを目にしていたかどうかを確認でき、それがノイズかどうかを判断できます。同じトリアージの考え方は、「Failed to Fetch Dynamically Imported Module」のような、本番環境で発生するその他のノイズの多いエラーにも当てはまります。
まとめ
ほとんどの場合、「ResizeObserver loop completed」エラーは、ブラウザが一部の通知を後回しにし、1 フレーム後にレイアウトが安定したことを意味します。レイアウトが安定するかどうか、そして目に見えるガタつきがないかどうかを確認してください。どちらも問題なければ、トラッカーで両方のメッセージをフィルタリングします。問題がある場合は、監視対象の要素をリサイズしているコールバックを特定し、遅延処理または期待サイズによるガードで修正してください。
よくある質問
requestAnimationFrame よりも setTimeout や debounce のほうが優れた修正方法ですか?
いいえ。どちらもリサイズの書き込みを現在のフレームの外に移すため、エラーは発生しなくなりますが、要素を拡大し続けるループを止めることはできません。setTimeout のコールバックはブラウザがすでにペイントした後に実行される可能性があるため、変更が反映される前の 1 フレームの間、ユーザーに古いレイアウトが見えてしまうことがあります。requestAnimationFrame は次のペイントの前に実行されるため、更新をレンダリングと同期させることができます。
なぜコンポーネントのマウント直後にエラーが発生するのですか?
ResizeObserver は、レンダリング済みでサイズがゼロでない要素に対して observe() が呼ばれると、最初の通知を送信します。この最初の通知でコールバックがその要素に新しいサイズを書き込むと、ブラウザは後続の通知を後回しにし、即座にエラーを報告します。マウント時に自身を計測してリサイズするコンポーネントは、ページ上で何もリサイズされていなくても、この仕組みによってエラーを発生させます。
CSS コンテナクエリで ResizeObserver のコールバックを置き換えられますか?
はい。コールバックがコンテナの幅に応じてスタイルを変更するだけであれば置き換えられます。コンテナサイズクエリはサイズ包含(size containment)を適用するため、コンテナはその内容からサイズを決定せず、スタイリングが計測結果に影響を及ぼすことはありません。サイズクエリは Chrome 105、Safari 16、Firefox 110 以降で動作します。チャートの寸法の再計算や仮想化された行数の算出など、CSS では実現できない処理には引き続き ResizeObserver を使いましょう。
スタックトレースが空の場合、エラーを引き起こしているコードをどう特定すればよいですか?
開発環境で、window.ResizeObserver を、監視対象の各要素をログ出力しコールバックをラップするサブクラスに一時的に置き換えます。その後エラーを再現し、window のエラーイベントの直前にどの要素がリサイズされたかを確認します。コールバックがサードパーティライブラリ内にある場合は、エラーが発生しなくなるまでコンポーネントを 1 つずつ取り除いていくことで、発生源を絞り込めます。