パッシブ/アクティブイベントとスクロール性能の関係
スクロールがカクつく原因を断ち、指の動きに張り付く滑らかさを取り戻せる。passiveリスナがpreventDefaultを封じられる理由と、コンポジタスクロールを妨げない設計を内部から解説します。
- 1.touchstart/touchmove/wheel などスクロール起点のイベントは、リスナ内で preventDefault を呼べるためブラウザはコンポジタスレッドでのスクロールを即座に開始できず、リスナの完了を待つ。passive:true はこの「preventDefault を呼ばない」という約束で、ブラウザはリスナと並行してスクロールを進められる。
- 2.passive リスナ内で preventDefault を呼んでも無視され、コンソール警告が出るだけでスクロールは止まらない。スクロールを実際に抑止したいときだけ passive:false(アクティブ)を明示し、それ以外は passive に倒すのが原則。
- 3.近年のブラウザは、document/window/body 直付けの touchstart/touchmove/wheel/mousewheel リスナを既定で passive 扱いにする。スクロールを止める設計に依存しているなら、明示的に passive:false を指定しない限り preventDefault が効かなくなる点に注意する。
スクロールがリスナを待つという問題
指でスクロールを始めた瞬間、ブラウザは画面を動かしたい。ところがページに touchstart や wheel のリスナが登録されていると、ブラウザはすぐには動かせません。理由は単純で、そのリスナが preventDefault() を呼んでスクロールを取り消すかもしれないからです。preventDefault() を呼ぶか呼ばないかは、リスナのコードを最後まで実行してみないと分かりません。
その結果、既定(passive を指定しない、いわゆるアクティブリスナ)の挙動はこうなります。
1. ユーザーが指を動かす / ホイールを回す
2. ブラウザは touchmove / wheel リスナをメインスレッドで実行
3. リスナが終わるまで待つ(preventDefault されるか不明なため)
4. preventDefault されなかったと確認 → ようやくスクロール開始
問題は手順3です。リスナの実行はメインスレッドで行われ、そこにレイアウトや他のJSが詰まっていれば、スクロールの開始がそのぶん遅れる。指は動いているのに画面が付いてこない、という典型的なジャンクの正体がこれです。スクロール自体は本来コンポジタスレッドで進められるのに、preventDefault() の可能性を確かめるためだけにメインスレッドの完了を待たされているわけです。
touchstart も対象になるのは、最初のタッチ時点で preventDefault() を呼ぶと、それに続く touchmove 列ごとスクロールを丸ごとキャンセルできる仕様だからです。つまり touchstart のリスナ1つでスクロール全体を止め得るため、ブラウザは touchstart の完了も待つ必要があります。wheel / mousewheel も同じ理屈で、リスナが既定動作(スクロール)を打ち消す権限を持つイベントはすべて待ち合わせの対象になります。
passive がリスナとスクロールを切り離す
addEventListener の第3引数オプション {passive: true} は、ブラウザへの**「このリスナは preventDefault() を呼びません」という約束**です。約束が立つと、ブラウザはリスナの完了を待つ必要がなくなります。
// passive:true — preventDefault しないと宣言。スクロールはブロックされない
window.addEventListener('touchmove', onTouchMove, { passive: true });
// passive:false(アクティブ)— preventDefault する可能性を残す。スクロールが待たされる
window.addEventListener('touchmove', onTouchMove, { passive: false });
passive 指定があると、ブラウザはリスナをメインスレッドで走らせつつ、スクロールはコンポジタスレッドで並行して進められます。スクロールの合成自体がメインスレッドから独立して動く仕組みは レイヤー化とGPUコンポジット と同根で、passive はその独立性をスクロール開始判定にまで広げる宣言だと捉えると分かりやすいでしょう。
| リスナ種別 | preventDefault | スクロール開始 | リスナが重いと |
|---|---|---|---|
| passive: true | 呼べない(無視) | リスナと並行して即時 | スクロールは影響を受けない |
| passive: false(アクティブ) | 呼べる | リスナ完了後 | そのぶん遅延しジャンク |
決定的なのは、passive がスクロールの開始タイミングを変える点です。リスナの中身を軽くする最適化(処理の間引き等)はメインスレッドの仕事を減らしますが、それでも「リスナ完了を待つ」構造自体は消えません。passive はその待ち合わせ構造ごと取り払うため、効き方が質的に違います。
passive で preventDefault が効かない理由
passive リスナの中で preventDefault() を呼ぶと、その呼び出しは黙殺され、ブラウザはコンソールに「Unable to preventDefault inside passive event listener」といった警告を出します。スクロールは止まりません。
これは矛盾ではなく、約束の整合性を保つための必然です。ブラウザは passive 宣言を信じて、リスナの完了を待たずにスクロールをもう始めてしまっているかもしれない。そこから遡って「やっぱり止めて」を受け付けたら、約束が無意味になり、待ち合わせを省いた意味も消えます。だから passive リスナの preventDefault() は仕様上no-opと定められています。
ピンチズーム抑止やカスタムスクロール、touch-action を併用しないドラッグ実装などは preventDefault() に依存します。これらのリスナをうっかり passive にすると、エラーは投げられず警告とともに静かに無効化されるのが厄介です。「動かない原因が分からない」ときは、リスナの passive 指定と、後述の既定 passive 化をまず疑ってください。passive まわりのオプション解釈そのものは イベントリスナ登録の内部モデル に詳しくあります。
既定で passive 化される対象
スクロール性能を底上げするため、近年のブラウザは一部のリスナを既定で passive 扱いにするようになりました。Chrome や Firefox では、window / document / document.body に直接登録された touchstart / touchmove(および wheel / mousewheel)リスナの passive 既定値が、未指定のとき false ではなく true に切り替わっています。
// 旧来の意図:このリスナでスクロールを止めたい
// だが document 直付けで passive 未指定だと、既定 passive:true により
// preventDefault が効かず、警告だけ出てスクロールは止まらない
document.addEventListener('touchmove', e => {
e.preventDefault(); // 既定 passive 化された環境では無視される
});
// 正しい:止める意図があるなら passive:false を明示する
document.addEventListener('touchmove', e => {
e.preventDefault();
}, { passive: false });
既定 passive 化は、あくまで window / document / body といったルート付近のスクロール関連イベントに限った緩和です。任意の子要素への登録や、touchstart / touchmove / wheel 以外のイベントには適用されません。「全部 passive になった」と誤解すると別のバグを生みます。意図して既定動作を打ち消すリスナでは、対象とイベントを問わず passive:false を明示するのが安全です。
コンポジタスクロールを妨げない設計
passive を正しく使うことは、スクロールをメインスレッドから解放してコンポジタスレッドに任せる設計と同義です。指針は次の通りです。
- デフォルトは passive に倒す:スクロール監視・遅延読み込み・視差効果など「観測するだけ」のリスナは、すべて
{passive: true}を明示する。既定 passive 化に頼らず明示することで、子要素登録でも一貫した挙動になります。 - 止めるときだけ
passive:false:preventDefault()でスクロールを実際に抑止する箇所のみアクティブにする。範囲は最小限に絞る。 - 抑止は
touch-actionを第一候補に:横スクロールだけ許す・ズームを禁じるといった意図は、JSのpreventDefault()ではなくCSSのtouch-action(例touch-action: pan-y)で宣言する。これはコンポジタ側で完結し、リスナをアクティブにせずに済みます。 - リスナの中身も軽く保つ:passive でもリスナはメインスレッドで走ります。スクロールハンドラ内での読み取りがレイアウトを強制同期させないよう、計測は IntersectionObserver/ResizeObserver に寄せると、ジャンク要因をさらに減らせます。
// 観測専用ハンドラは passive、レイアウト読みは観測APIに逃がす
window.addEventListener('scroll', updateProgressBar, { passive: true });
// ズーム禁止・縦スクロールのみ許可は CSS で宣言(JS の preventDefault 不要)
// .stage { touch-action: pan-y; }
passive:true は「いつスクロールを始めるか」を早める宣言、touch-action は「どのスクロール/ジェスチャを許すか」を宣言する仕組みです。前者はリスナの待ち合わせを外し、後者は既定動作の許可範囲をコンポジタ側で確定させます。両者を組み合わせると、JSで preventDefault() を握りしめなくても、滑らかさと制御の両立ができます。
まとめ
イベント問では、(1) touchstart / touchmove / wheel は preventDefault() でスクロールを打ち消せるため、アクティブリスナだとブラウザがリスナ完了を待ってからスクロールを始めること、(2) passive:true は「preventDefault しない」約束で、リスナとスクロールを並行させコンポジタスレッドに任せられること、(3) passive リスナ内の preventDefault() は no-op で警告のみ、(4) window/document/body のスクロール関連リスナは既定 passive 化され、止めたいなら passive:false 明示が要ること、の4点が頻出です。
スクロール起点のイベントは preventDefault() で既定動作を打ち消せるため、アクティブリスナだとブラウザはリスナ完了を待ってからスクロールを開始します。これがメインスレッドの混雑をスクロール遅延に直結させる原因です。{passive: true} は「preventDefault しない」という約束で、ブラウザはリスナと並行してコンポジタスレッドでスクロールを進められるようになります。代償として passive リスナ内の preventDefault() は無視されるため、スクロールを実際に止める箇所だけ passive:false を明示し、ズーム/方向制御はできるだけ touch-action に寄せるのが正解です。近年の既定 passive 化も踏まえ、観測系は passive・抑止系は明示と切り分けてください。合成側の独立性は レイヤー化とGPUコンポジット、プロパティごとの重さの仕分けは リフローとリペイントのコスト を合わせて押さえると、スクロール性能を原理から制御できます。
Web/フロントエンド Article
パッシブ/アクティブイベントとスクロール性能の関係を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
イベント
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
passive リスナ内で preventDefault を呼んでも無視され、コンソール警告が出るだけでスクロールは止まらない。スクロールを実際に抑止したいときだけ passive:false(アクティブ)を明示し、それ以外は passive に倒すのが原則。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「イベント / スクロール」に近いか確認する。
- 強みである「touchstart/touchmove/wheel などスクロール起点のイベントは、リスナ内で preventDefault を呼べるためブラウザはコンポジタスレッドでのスクロールを即座に開始できず、リスナの完了を待つ。passive:true はこの「preventDefault を呼ばない」という約束で、ブラウザはリスナと並行してスクロールを進められる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。