コンポジタへの入力
全 4 回にわたるブログシリーズの最後です。Chrome の内部を見ると、ウェブサイトを表示するためのコードが Chrome でどのように処理されるかがわかります。前回の投稿では、レンダリング プロセスとコンポジタについて学びました。この投稿では、コンポジタがユーザー入力を受け取ったときにスムーズなインタラクションを実現する仕組みについて説明します。
ブラウザの視点からの入力イベント
「入力イベント」と聞くと、テキスト ボックスへの入力やマウスクリックしか思い浮かばないかもしれませんが、ブラウザの観点から見ると、入力とはユーザーからのあらゆる操作を指します。マウスホイールのスクロールは入力イベントであり、タップまたはマウスオーバーも入力イベントです。
画面のタップなどのユーザー操作が発生すると、最初に操作を受け取るのはブラウザ プロセスです。ただし、タブ内のコンテンツはレンダラ プロセスによって処理されるため、ブラウザ プロセスは、そのジェスチャーが発生した場所のみを認識します。そのため、ブラウザ プロセスはイベントタイプ(touchstart
など)とその座標をレンダラ プロセスに送信します。レンダラ プロセスは、イベント ターゲットを見つけて、接続されているイベント リスナーを実行することで、イベントを適切に処理します。
コンポーザが入力イベントを受信する
前回の投稿では、コンポジタがラスタライズされたレイヤを合成することでスクロールをスムーズに処理する方法について説明しました。ページに入力イベント リスナーがアタッチされていない場合、Compositor スレッドは、メインスレッドから完全に独立した新しい複合フレームを作成できます。ただし、一部のイベント リスナーがページに接続されている場合はどうなりますか?コンポジタ スレッドは、イベントを処理する必要があるかどうかをどのように判断しますか?
高速スクロール不可の領域について
JavaScript の実行はメインスレッドのジョブであるため、ページが合成されると、コンポジタ スレッドは、イベント ハンドラがアタッチされているページの領域を「高速スクロール不可の領域」としてマークします。この情報により、コンポジタ スレッドは、イベントがその領域で発生した場合に、メインスレッドに入力イベントを送信できます。この領域の外部から入力イベントが届いた場合、コンポジタ スレッドはメインスレッドを待たずに新しいフレームの合成を続けます。
イベント ハンドラを作成する際の注意事項
ウェブ開発で一般的なイベント処理パターンは、イベント委任です。イベントはバブルアップするため、最上位の要素に 1 つのイベント ハンドラを接続し、イベント ターゲットに基づいてタスクを委任できます。次のようなコードを見たり書いたりしたことがあるかもしれません。
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault();
}
});
すべての要素に対して 1 つのイベント ハンドラを記述するだけでよいため、このイベント委任パターンのエルゴノミクスが魅力的です。ただし、ブラウザの視点からこのコードを見ると、ページ全体が高速スクロール不可の領域としてマークされます。つまり、アプリケーションがページの特定の部分からの入力を気にしない場合でも、コンポジタ スレッドは入力イベントが届くたびにメインスレッドと通信して待機する必要があります。そのため、コンポジターのスムーズなスクロール機能が機能しなくなります。
これを軽減するには、イベント リスナーで passive: true
オプションを渡します。これにより、メインスレッドでイベントをリッスンしたいが、コンポジタは新しいフレームの合成も続行できるというヒントがブラウザに示されます。
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});
イベントをキャンセルできるかどうかを確認する
ページ内に、スクロール方向を水平スクロールのみに制限するボックスがあるとします。
ポインタ イベントで passive: true
オプションを使用すると、ページのスクロールがスムーズになりますが、スクロール方向を制限するために preventDefault
を実行するまでに縦方向のスクロールが開始されている可能性があります。これは、event.cancelable
メソッドを使用して確認できます。
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
/*
* do what you want the application to do here
*/
}
}, {passive: true});
または、touch-action
などの CSS ルールを使用して、イベント ハンドラを完全に削除することもできます。
#area {
touch-action: pan-x;
}
イベント ターゲットを見つける
コンポジタ スレッドが入力イベントをメインスレッドに送信すると、最初にヒットテストが実行され、イベント ターゲットが検索されます。ヒットテストは、レンダリング プロセスで生成されたペイント レコード データを使用して、イベントが発生したポイント座標の下にあるものを探します。
メインスレッドへのイベント ディスパッチを最小限に抑える
前回の投稿では、一般的なディスプレイが 1 秒間に 60 回画面を更新する仕組みと、スムーズなアニメーションを実現するためにそのペースに合わせる必要がある仕組みについて説明しました。入力の場合、一般的なタッチ スクリーン デバイスは 1 秒あたり 60 ~ 120 回、一般的なマウスでは 1 秒あたり 100 回のタッチイベントを配信します。入力イベントの忠実度が、画面の更新頻度よりも高い。
touchmove
などの連続イベントが 1 秒あたり 120 回メインスレッドに送信された場合、画面の更新速度と比較して、ヒットテストと JavaScript の実行が過剰にトリガーされる可能性があります。
メインスレッドへの過剰な呼び出しを最小限に抑えるために、Chrome は連続するイベント(wheel
、mousewheel
、mousemove
、pointermove
、touchmove
など)を統合し、次の requestAnimationFrame
の直前までディスパッチを遅らせます。
keydown
、keyup
、mouseup
、mousedown
、touchstart
、touchend
などの離散イベントは、すぐにディスパッチされます。
getCoalescedEvents
を使用してフレーム内イベントを取得する
ほとんどのウェブ アプリケーションでは、統合イベントで十分に優れたユーザー エクスペリエンスを提供できます。ただし、描画アプリケーションなどのものを構築し、touchmove
座標に基づいてパスを配置する場合は、滑らかな線を描画するために中間の座標が失われる可能性があります。その場合は、ポインタ イベントの getCoalescedEvents
メソッドを使用して、統合されたイベントに関する情報を取得できます。
window.addEventListener('pointermove', event => {
const events = event.getCoalescedEvents();
for (let event of events) {
const x = event.pageX;
const y = event.pageY;
// draw a line using x and y coordinates.
}
});
次のステップ
このシリーズでは、ウェブブラウザの内部の仕組みについて説明しました。DevTools でイベント ハンドラに {passive: true}
を追加することを推奨している理由や、スクリプトタグに async
属性を記述する理由について考えたことがない場合は、このシリーズで、ブラウザが高速でスムーズなウェブ エクスペリエンスを提供するためにこれらの情報が必要である理由について理解を深めていただければ幸いです。
Lighthouse を使用する
ブラウザに優しいコードを作成したいが、どこから始めればよいかわからない場合は、Lighthouse を使用してください。Lighthouse は、ウェブサイトの監査を実行し、問題なく動作している点と改善が必要な点に関するレポートを生成します。監査リストを確認すると、ブラウザが重視している点も把握できます。
パフォーマンスを測定する方法
パフォーマンスの調整はサイトによって異なる可能性があるため、サイトのパフォーマンスを測定して、サイトに最適なものを決定することが重要です。Chrome DevTools チームには、サイトのパフォーマンスを測定する方法に関するチュートリアルがいくつか用意されています。
サイトに Feature Policy を追加する
さらに一歩進んだ対策として、機能ポリシーという新しいウェブ プラットフォーム機能があります。これは、プロジェクトの構築時にガイドとして使用できます。機能ポリシーをオンにすると、アプリの特定の動作が保証され、間違いを防ぐことができます。たとえば、アプリが解析をブロックしないようにするには、同期スクリプト ポリシーでアプリを実行します。sync-script: 'none'
が有効になっている場合、パーサー ブロック JavaScript は実行されません。これにより、コードがパーサーをブロックすることを防ぐことができ、ブラウザはパーサーの一時停止を気にする必要がなくなります。
まとめ
ウェブサイトの構築を始めた頃は、コードの書き方と生産性の向上にのみ関心がありました。これらは重要ですが、ブラウザが作成したコードをどのように処理するかについても考える必要があります。最新のブラウザは、ユーザーに優れたウェブ エクスペリエンスを提供するために、これまでも、そして今後も投資を続けています。コードを整理してブラウザに優しいコードにすることで、ユーザー エクスペリエンスが向上します。ブラウザの使い方をするためのクエストにぜひご協力ください。
このシリーズの初期ドラフトを確認していただいた皆様(Alex Russell、Paul Irish、Meggin Kearney、Eric Bidelman、Mathias Bynens、Addy Osmani、Kinuko Yasuda、Nasko Oskov、Charlie Reis など)に心より感謝いたします。
このシリーズはいかがでしたか?今後の記事についてご質問やご提案がございましたら、以下のコメント欄または Twitter の @kosamari までお寄せください。