touchやwheelのイベントリスナーにpassive: trueを明示的に指定する必要があるのか調べてみた

## これは何

[EventTarget.addEventListener() #パッシブリスナーによるスクロールの性能改善 - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener#%E3%83%91%E3%83%83%E3%82%B7%E3%83%96%E3%83%AA%E3%82%B9%E3%83%8A%E3%83%BC%E3%81%AB%E3%82%88%E3%82%8B%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AE%E6%80%A7%E8%83%BD%E6%94%B9%E5%96%84) にあるように、以下のようなイベントに対してイベントリスナーを設定すると、スクロールの処理性能が大幅に低下する可能性があります。

- touchstart
- touchmove
- wheel
- mousewheel

それに対して、`passive`オプションを`true`で設定するとこの問題を防ぐことができるようですが、一部のブラウザではデフォルトで`passive: true`になっているということで、毎回明示的に設定する必要があるのかどうか調べてみました。

## 結論

- ChromeやFirefoxではデフォルトで`passive: true`になっている (window, document, document.bodyが対象のとき)
  - [EventTarget.addEventListener() # ブラウザーの互換性 - Web API | MDN](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)
- MDNの互換性の表では`No`になっているmacOS Safari(15.3)でも
  - touchイベント: Safari 11.1でデフォルトpassiveになっていそう (実機で試してはいない)
    - [Safari 11.1](https://developer.apple.com/library/archive/releasenotes/General/WhatsNewInSafari/Articles/Safari_11_1.html)
  - 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](https://github.com/facebook/react/pull/19654)
  - ただし、逆に`passive: true`にする方法がないため、これらのイベントに対して`e.preventDefault()`をすることができない
    - [React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react](https://github.com/facebook/react/issues/22794)

という前提で必要なときに指定する

## `passive: true`について

touchやwheelイベントはイベントリスナーを設定すると、スクロールをする前にスクロールをブロックする処理がないかを確認するため、非同期のスクロールがブロックされてスクロールがカクつくという、いわゆるScroll Jankが発生します。[^1]

[^1]: https://dom.spec.whatwg.org/#observing-event-listeners

それを防ぐために、スクロールをブロックする処理 (`e.preventDefailt()`) がない場合は`passive: true`というオプションをイベントリスナー設定時に付与してあげることでScroll Jankを防ぎ、スクロールをスムーズに行うことができるようになります。

## ブラウザデフォルトの挙動について

実はこのtouchやwheelの一部イベントの`passive: true`オプションですが、ChromeやFirefoxではデフォルトで`passive: true`になっています。[^2] (対象はwindow, document, document.body[^4])

[^2]: 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
[^4]: https://developers.google.com/web/updates/2017/01/scrolling-intervention

しかし[MDNの互換性の表](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)を見てみると、Safariはデフォルトが`true`になっていないようです。

### Safariでの挙動について

macOS Safari version: 15.3

実際に以下のようなコードを実装し、Safariで動作確認をしてみます。

```js
document.addEventListener('wheel', (e) => {
  e.preventDefault()
  console.log('handler called')
});
```

確認をしてみると、スクロールは無効化されず、普通にスクロールができてしまいました。

次に明示的に`passive: false`を渡してみます。

```js
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`になっているようです。[^3]

[^3]: https://developer.apple.com/library/archive/releasenotes/General/WhatsNewInSafari/Articles/Safari_11_1.html

## Reactでの挙動

React 17では[こちらのissue](https://github.com/facebook/react/issues/19651)でデフォルトの挙動について議論がされた後、`passive: true`がデフォルトになったようです。
対象のイベントハンドラは、`onTouchStart`, `onTouchMove`, `onWheel`です。[^5]

[^5]: https://github.com/facebook/react/pull/19654/files

ただ、`passive: true`になったのはいいのですが、現時点(react@17.0.2)ではこの`passive`を操作できるオプションが存在していないようで、逆に`passive: true`にすることができない問題もあるようです。これにより、`onTouchStart`, `onTouchMove`, `onWheel`のイベントハンドラ内では`e.preventDefault()`でイベントをブロックできないようです。

```jsx
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](https://github.com/facebook/react/issues/22794))ようなので、今後対応されるかもしれません。

## まとめ

- ChromeやFirefoxではデフォルトで`passive: true`になっている (window, document, document.bodyが対象のとき)
  - [EventTarget.addEventListener() # ブラウザーの互換性 - Web API | MDN](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)
- MDNの互換性の表では`No`になっているmacOS Safari(15.3)でも
  - touchイベント: Safari 11.1でデフォルトpassiveになっていそう (実機で試してはいない)
    - [Safari 11.1](https://developer.apple.com/library/archive/releasenotes/General/WhatsNewInSafari/Articles/Safari_11_1.html)
  - 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](https://github.com/facebook/react/pull/19654)
  - ただし、逆に`passive: true`にする方法がないため、これらのイベントに対して`e.preventDefault()`をすることができない
    - [React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react](https://github.com/facebook/react/issues/22794)

という前提で必要なときに指定する

## 参考

- MDN
  - [EventTarget.addEventListener() #パッシブリスナーによるスクロールの性能改善](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener#%E3%83%91%E3%83%83%E3%82%B7%E3%83%96%E3%83%AA%E3%82%B9%E3%83%8A%E3%83%BC%E3%81%AB%E3%82%88%E3%82%8B%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AE%E6%80%A7%E8%83%BD%E6%94%B9%E5%96%84)
- Chrome
  - [Making touch scrolling fast by default  |  Web  |  Google Developers](https://developers.google.com/web/updates/2017/01/scrolling-intervention)
- Safari
  - [Safari 11.1](https://developer.apple.com/library/archive/releasenotes/General/WhatsNewInSafari/Articles/Safari_11_1.html)
  - [218842 – Force wheel event listeners on the root to be passive](https://bugs.webkit.org/show_bug.cgi?id=218842)
- React
  - [Touch/Wheel Event Passiveness in React 17 · Issue #19651 · facebook/react](https://github.com/facebook/react/issues/19651)
  - [Keep onTouchStart, onTouchMove, and onWheel passive by gaearon · Pull Request #19654 · facebook/react](https://github.com/facebook/react/pull/19654)
  - [React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react](https://github.com/facebook/react/issues/22794)