これは何
EventTarget.addEventListener() #パッシブリスナーによるスクロールの性能改善 - Web API | MDN にあるように、以下のようなイベントに対してイベントリスナーを設定すると、スクロールの処理性能が大幅に低下する可能性があります。
- touchstart
- touchmove
- wheel
- mousewheel
それに対して、passive
オプションをtrue
で設定するとこの問題を防ぐことができるようですが、一部のブラウザではデフォルトでpassive: true
になっているということで、毎回明示的に設定する必要があるのかどうか調べてみました。
結論
- ChromeやFirefoxではデフォルトで
passive: true
になっている (window, document, document.bodyが対象のとき) - MDNの互換性の表では
No
になっているmacOS Safari(15.3)でも- touchイベント: Safari 11.1でデフォルトpassiveになっていそう (実機で試してはいない)
- wheelイベント: 「実験的な機能」の中に、
Wheel Event listeners on the root made passive
があり、デフォルトオンになっている
- React17でイベントハンドラを設定する場合もデフォルトで
passive: true
になっている- Keep onTouchStart, onTouchMove, and onWheel passive by gaearon · Pull Request #19654 · facebook/react
- ただし、逆に
passive: true
にする方法がないため、これらのイベントに対してe.preventDefault()
をすることができない
という前提で必要なときに指定する
passive: true
について
touchやwheelイベントはイベントリスナーを設定すると、スクロールをする前にスクロールをブロックする処理がないかを確認するため、非同期のスクロールがブロックされてスクロールがカクつくという、いわゆるScroll Jankが発生します。1
それを防ぐために、スクロールをブロックする処理 (e.preventDefailt()
) がない場合はpassive: true
というオプションをイベントリスナー設定時に付与してあげることでScroll Jankを防ぎ、スクロールをスムーズに行うことができるようになります。
ブラウザデフォルトの挙動について
実はこのtouchやwheelの一部イベントのpassive: true
オプションですが、ChromeやFirefoxではデフォルトでpassive: true
になっています。2 (対象はwindow, document, document.body3)
しかしMDNの互換性の表を見てみると、Safariはデフォルトがtrue
になっていないようです。
Safariでの挙動について
macOS Safari version: 15.3
実際に以下のようなコードを実装し、Safariで動作確認をしてみます。
document.addEventListener('wheel', (e) => {
e.preventDefault()
console.log('handler called')
});
確認をしてみると、スクロールは無効化されず、普通にスクロールができてしまいました。
次に明示的にpassive: false
を渡してみます。
document.addEventListener('wheel', (e) => {
e.preventDefault()
console.log('handler called')
}, { passive: false });
すると予想通りスクロールを無効化することができました。
実はSafariの「実験的な機能」の中に、Wheel Event listeners on the root made passive
があり、オンになっていることでこのような挙動になっていたようでした。これはSafari15.3ではデフォルトでオンのようです。 (いつからこうなっているのかまでは調べきれませんでした)
試しにオフにしてみると、最初のコードでもスクロールが無効化されました。
また、touchイベントも、Safari 11.1からデフォルトでpassive: true
になっているようです。4
Reactでの挙動
React 17ではこちらのissueでデフォルトの挙動について議論がされた後、passive: true
がデフォルトになったようです。
対象のイベントハンドラは、onTouchStart
, onTouchMove
, onWheel
です。5
ただ、passive: true
になったのはいいのですが、現時点(react@17.0.2)ではこのpassive
を操作できるオプションが存在していないようで、逆にpassive: true
にすることができない問題もあるようです。これにより、onTouchStart
, onTouchMove
, onWheel
のイベントハンドラ内ではe.preventDefault()
でイベントをブロックできないようです。
const handler = (e) => {
e.preventDefault() // スクロールを止めることはできない
console.log('handler called')
}
const App = () => {
return (
<div onWheel={e => handler(e)}>
<Contents />
</div>
);
}
これについては、issueも存在している(React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react)ようなので、今後対応されるかもしれません。
まとめ
- ChromeやFirefoxではデフォルトで
passive: true
になっている (window, document, document.bodyが対象のとき) - MDNの互換性の表では
No
になっているmacOS Safari(15.3)でも- touchイベント: Safari 11.1でデフォルトpassiveになっていそう (実機で試してはいない)
- wheelイベント: 「実験的な機能」の中に、
Wheel Event listeners on the root made passive
があり、デフォルトオンになっている
- React17でイベントハンドラを設定する場合もデフォルトで
passive: true
になっている- Keep onTouchStart, onTouchMove, and onWheel passive by gaearon · Pull Request #19654 · facebook/react
- ただし、逆に
passive: true
にする方法がないため、これらのイベントに対してe.preventDefault()
をすることができない
という前提で必要なときに指定する
参考
- MDN
- Chrome
- Safari
- React
-
https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7 ↩
-
https://developers.google.com/web/updates/2017/01/scrolling-intervention ↩
-
https://developer.apple.com/library/archive/releasenotes/General/WhatsNewInSafari/Articles/Safari_11_1.html ↩