IntersectionObserverとResizeObserverの非同期計測モデル
スクロール連動やサイズ追従を、強制同期レイアウトもカクつきも起こさず実装できるようになる。交差・サイズ変化をレイアウト後に非同期通知する仕組みと、ResizeObserverのループ抑止の原理を解説します。
- 1.両Observerは計測を毎フレームのレイアウト確定後に行い、結果をまとめて非同期コールバックで配るため、scroll/resizeイベント方式と違い計測がメインスレッドの読み書きと衝突せずレイアウトスラッシングを起こさない。
- 2.ResizeObserverのコールバックは「レイアウト後・ペイント前」に走り、その中でサイズを変えると同フレームで再レイアウト→再通知が要る。仕様は深さ(depth)で多重通知を制御し、無限ループは打ち切って『ResizeObserver loop limit exceeded』を出す。
- 3.IntersectionObserverは交差判定をrAFと同期したタイミングでバッチ計算し、isIntersecting・intersectionRatio・各矩形を確定済みジオメトリから返すため、コールバック内でgetBoundingClientRectを呼ぶ必要がなく強制同期レイアウトを避けられる。
なぜ「イベント」ではなく「Observer」なのか
要素が画面に入ったか、要素の寸法が変わったかを知りたいとき、素朴には scroll / resize イベントを購読し、その中で getBoundingClientRect() を呼んで判定します。しかしこの方式は二重に高コストです。第一に、これらのイベントは同期的に高頻度で発火し、ハンドラがメインスレッドを奪います。第二に、ハンドラ内でジオメトリを読むと、保留中のスタイル変更を確定させるための強制同期レイアウトが走り、書き込みと交互になればレイアウトスラッシングに直結します(この往復構造は リフローとリペイントのコスト で詳述)。
IntersectionObserver と ResizeObserver は、この計測をブラウザ側に寄せて非同期化する設計です。判定はブラウザがレイアウトを確定させた後に1回まとめて行い、結果だけをコールバックで配ります。アプリ側はジオメトリを読みに行かないので、強制同期レイアウトの起点が消えます。
| 観点 | scroll/resize + getBoundingClientRect | Observer |
|---|---|---|
| 計測の主体 | アプリのハンドラが同期で読む | ブラウザがレイアウト確定後に計算 |
| 発火頻度 | イベントごと(高頻度・無制限) | フレーム単位でバッチ |
| 強制同期レイアウト | 起きやすい(読み書き往復) | アプリは読まないので起きない |
| 通知のタイミング | 同期(イベントループのその場) | 非同期(専用のチェックステップ) |
レンダリングループのどこで計測されるか
両Observerの計測は、HTMLの**レンダリング更新(update the rendering)**という一連のステップの一部として、フレームごとに走ります。1フレームの大まかな並びは次の通りです。
1. タスク/マイクロタスクの実行(JS)
2. requestAnimationFrame コールバック
3. スタイル計算 → レイアウト確定
4. ResizeObserver の処理(gather → broadcast)
5. IntersectionObserver の交差判定(描画機会ごと)
6. ペイント → コンポジット
ポイントは、計測がレイアウト確定後に置かれていることです。この時点でブラウザは各要素の最新の矩形を内部に持っているため、Observerは追加のレイアウトを起こさずにサイズや交差を読めます。アプリが getBoundingClientRect() を呼ぶと保留レイアウトを完了させてしまうのに対し、Observerは「すでに確定した値」を参照するだけ、という非対称がコストの差を生みます。各ステップがどのフェーズに属するかは レンダリングパイプライン詳説 と、コールバックの実行順序という観点では イベントループの内部構造 が背景になります。
Observerのコールバックは、Promise のマイクロタスクキューにも setTimeout のタスクキューにも乗りません。レンダリングループ内の専用ステップ(ResizeObserver は「broadcast active observations」、IntersectionObserver は「notify intersection observers」)で、その場のレイアウト結果を引数に呼ばれます。だから「次のフレームの描画直前」という安定したタイミングが保証され、計測値とコールバック実行のズレが生じません。
ResizeObserverのループと深さ(depth)
ResizeObserver 固有の難しさは、コールバック内でサイズを変えられる点にあります。観測中の要素のサイズが変わったからコールバックが呼ばれ、その中でDOMをいじって別の要素(あるいは自分)のサイズを変えると、同じフレーム内で再びレイアウトが必要になり、新しいサイズをまた通知しなければなりません。素朴に作ると無限ループになります。
仕様はこれを深さ(depth)という概念で制御します。1フレーム内で「レイアウト→アクティブな観測を収集→コールバックでブロードキャスト」を繰り返しますが、各反復で通知する対象を、前回より深い位置(DOMツリーで下)にある要素だけに絞ります。これにより、親のサイズ変化が子に波及する一方向の連鎖は同フレーム内で収束し、無限の往復だけが排除されます。
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
// contentBoxSize / borderBoxSize は確定済みの寸法。
// ここで再度 getBoundingClientRect を呼ぶ必要はない。
const w = entry.contentBoxSize[0].inlineSize;
layout(entry.target, w); // ← ここで要素のサイズを変えると再通知が要る
}
});
それでも収束しない書き方(コールバックのたびに浅い要素のサイズを揺らし続ける等)を作ると、ブラウザは安全のためにそのフレームでの追加処理を打ち切り、コンソールに ResizeObserver loop limit exceeded(または loop completed with undelivered notifications)を出します。これはクラッシュではなくループ抑止が働いた合図で、残りの通知は次のフレームに繰り越されます。
このエラーは「コールバック内のサイズ変更が同フレームで収束していない」サインです。対処は、(1) コールバック内での同期的なサイズ変更を避ける、(2) どうしても必要なら更新を requestAnimationFrame に逃がして次フレームへ回す、(3) 観測対象と書き込み対象を分け、書き込みが観測対象のサイズへ直接フィードバックしないように設計する、のいずれかです。無視しても致命的でないことは多いですが、毎フレーム揺れ続けているなら描画が1フレーム遅延している証拠です。
IntersectionObserverの判定モデル
IntersectionObserver は、ターゲット矩形とルート矩形(既定はビューポート、root 指定で任意のスクロールコンテナ)の交差を計算します。判定は描画の機会ごとにバッチで行われ、交差比率が threshold で指定した境界を跨いだときにだけコールバックを積みます。連続スクロール中に毎ピクセル発火するのではなく、しきい値の通過という離散イベントに変換されるのが効率の核です。
エントリは確定済みのジオメトリを持って渡されます。
isIntersecting : ルートと少しでも交差しているか(boolean)
intersectionRatio : ターゲット面積に対する交差面積の比(0〜1)
boundingClientRect : ターゲットの矩形
intersectionRect : 実際に交差している矩形
rootBounds : ルートの矩形
rootMargin はルート矩形を膨らませ/縮めてから交差を取るための余白で、画像の遅延読み込みで「画面に入る少し手前で読み始める」先読みに使います。intersectionRatio はターゲット面積を分母にした比なので、ターゲットがルートより大きい場合は完全表示でも 1 に達しない、という直感に反する挙動も理解しておく価値があります。
交差矩形は、単純なルートとの重なりだけでなく、途中のスクロールコンテナや overflow によるクリップも考慮して算出されます。つまり「ビューポートには入っているが、親の overflow: hidden で隠れている」要素は交差なしと判定され得ます。ジオメトリ計算をブラウザに任せる利点は、こうしたクリップ連鎖を自前で辿らずに済む点にもあります。
レイアウトスラッシングを避ける実装原則
両Observerを使う最大の動機は、計測フェーズと更新フェーズを物理的に分離できることです。Observerが「レイアウト後に確定値を配る」役を担うので、アプリのコールバックは読み取りをせず書き込みだけに専念できます。これは リフローとリペイントのコスト で述べた read→write 分離を、ブラウザの仕組みで強制した形と言えます。
| やってはいけない | 理由 | 正しい形 |
|---|---|---|
| コールバック内で getBoundingClientRect | 保留レイアウトを強制完了させる | entry の rect / size をそのまま使う |
| コールバック内で同期的にサイズ変更 | 同フレームで再レイアウト→再通知 | rAF へ逃がして次フレームで更新 |
| 1要素ごとに別Observerを生成 | 管理コストと通知の分散 | 1つのObserverで複数要素を observe |
実務では、IntersectionObserver は遅延読み込み・無限スクロール・表示計測(広告/アナリティクス)・スクロールスパイに、ResizeObserver はコンテナクエリ的な追従・キャンバスやチャートのリサイズ・要素単位のレイアウト調整に使います。いずれも scroll / resize のグローバルイベントを要素単位の宣言的な観測へ置き換えることで、ハンドラの間引き(throttle/debounce)を自前で書く必要をなくします。間引きはタイミングを荒くするだけで強制同期レイアウトの根を断てませんが、Observerは計測のタイミングそのものをレイアウト確定後に移すため、根本から問題を消します。
頻出は次の4点です。(1) 両Observerのコールバックはレイアウト確定後にバッチで非同期に呼ばれ、マイクロタスク/タスクとは別の専用ステップで走る。(2) ResizeObserverはdepthで同フレームの多重通知を制御し、収束しないと ResizeObserver loop limit exceeded で打ち切る。(3) IntersectionObserverは threshold の通過で離散発火し、intersectionRatio の分母はターゲット面積。(4) コールバック内でジオメトリを読まないことが強制同期レイアウト回避の要。
まとめ
IntersectionObserver と ResizeObserver は、交差とサイズ変化の計測をレイアウト確定後のバッチ処理としてブラウザに肩代わりさせ、確定済みのジオメトリをコールバックへ渡す非同期モデルです。アプリ側はジオメトリを読みに行かないため、scroll/resize ハンドラ方式で起きがちな強制同期レイアウトとレイアウトスラッシングを構造的に避けられます。ResizeObserverはdepthによる多重通知制御で連鎖を収束させ、収束しない場合は loop limit exceeded でループを抑止します。IntersectionObserverは threshold 通過の離散発火で高頻度スクロールを効率化します。背景の段構成は レンダリングパイプライン詳説、コールバック順序は イベントループの内部構造 と合わせて押さえてください。
Web/フロントエンド Article
IntersectionObserverとResizeObserverの非同期計測モデルを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
JavaScript
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
ResizeObserverのコールバックは「レイアウト後・ペイント前」に走り、その中でサイズを変えると同フレームで再レイアウト→再通知が要る。仕様は深さ(depth)で多重通知を制御し、無限ループは打ち切って『ResizeObserver loop limit exceeded』を出す。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「JavaScript / DOM」に近いか確認する。
- 強みである「両Observerは計測を毎フレームのレイアウト確定後に行い、結果をまとめて非同期コールバックで配るため、scroll/resizeイベント方式と違い計測がメインスレッドの読み書きと衝突せずレイアウトスラッシングを起こさない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。