これは何
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 ↩