最新のウェブブラウザの詳細(パート 4)

Mariko Kosaka

コンポジタへの入力

全 4 回にわたるブログシリーズの最後です。Chrome の内部を見ると、ウェブサイトを表示するためのコードが Chrome でどのように処理されるかがわかります。前回の投稿では、レンダリング プロセスとコンポジタについて学びました。この投稿では、コンポジタがユーザー入力を受け取ったときにスムーズなインタラクションを実現する仕組みについて説明します。

ブラウザの視点からの入力イベント

「入力イベント」と聞くと、テキスト ボックスへの入力やマウスクリックしか思い浮かばないかもしれませんが、ブラウザの観点から見ると、入力とはユーザーからのあらゆる操作を指します。マウスホイールのスクロールは入力イベントであり、タップまたはマウスオーバーも入力イベントです。

画面のタップなどのユーザー操作が発生すると、最初に操作を受け取るのはブラウザ プロセスです。ただし、タブ内のコンテンツはレンダラ プロセスによって処理されるため、ブラウザ プロセスは、そのジェスチャーが発生した場所のみを認識します。そのため、ブラウザ プロセスはイベントタイプ(touchstart など)とその座標をレンダラ プロセスに送信します。レンダラ プロセスは、イベント ターゲットを見つけて、接続されているイベント リスナーを実行することで、イベントを適切に処理します。

入力イベント
図 1: ブラウザ プロセスを経由してレンダラ プロセスに転送される入力イベント

コンポーザが入力イベントを受信する

図 2: ページレイヤにカーソルを合わせた際のビューポート

前回の投稿では、コンポジタがラスタライズされたレイヤを合成することでスクロールをスムーズに処理する方法について説明しました。ページに入力イベント リスナーがアタッチされていない場合、Compositor スレッドは、メインスレッドから完全に独立した新しい複合フレームを作成できます。ただし、一部のイベント リスナーがページに接続されている場合はどうなりますか?コンポジタ スレッドは、イベントを処理する必要があるかどうかをどのように判断しますか?

高速スクロール不可の領域について

JavaScript の実行はメインスレッドのジョブであるため、ページが合成されると、コンポジタ スレッドは、イベント ハンドラがアタッチされているページの領域を「高速スクロール不可の領域」としてマークします。この情報により、コンポジタ スレッドは、イベントがその領域で発生した場合に、メインスレッドに入力イベントを送信できます。この領域の外部から入力イベントが届いた場合、コンポジタ スレッドはメインスレッドを待たずに新しいフレームの合成を続けます。

高速スクロール不可の制限付き領域
図 3: 非高速スクロール可能領域への入力の図

イベント ハンドラを作成する際の注意事項

ウェブ開発で一般的なイベント処理パターンは、イベント委任です。イベントはバブルアップするため、最上位の要素に 1 つのイベント ハンドラを接続し、イベント ターゲットに基づいてタスクを委任できます。次のようなコードを見たり書いたりしたことがあるかもしれません。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

すべての要素に対して 1 つのイベント ハンドラを記述するだけでよいため、このイベント委任パターンのエルゴノミクスが魅力的です。ただし、ブラウザの視点からこのコードを見ると、ページ全体が高速スクロール不可の領域としてマークされます。つまり、アプリケーションがページの特定の部分からの入力を気にしない場合でも、コンポジタ スレッドは入力イベントが届くたびにメインスレッドと通信して待機する必要があります。そのため、コンポジターのスムーズなスクロール機能が機能しなくなります。

全画面表示の非高速スクロール可能領域
図 4: ページ全体をカバーする高速スクロール不可領域への記述入力の図

これを軽減するには、イベント リスナーで passive: true オプションを渡します。これにより、メインスレッドでイベントをリッスンしたいが、コンポジタは新しいフレームの合成も続行できるというヒントがブラウザに示されます。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

イベントをキャンセルできるかどうかを確認する

ページ スクロール
図 5: ページの一部が水平スクロールに固定されたウェブページ

ページ内に、スクロール方向を水平スクロールのみに制限するボックスがあるとします。

ポインタ イベントで 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;
}

イベント ターゲットを見つける

ヒットテスト
図 6: ペイント レコードを確認して x.y 点に何が描画されるかを尋ねるメインスレッド

コンポジタ スレッドが入力イベントをメインスレッドに送信すると、最初にヒットテストが実行され、イベント ターゲットが検索されます。ヒットテストは、レンダリング プロセスで生成されたペイント レコード データを使用して、イベントが発生したポイント座標の下にあるものを探します。

メインスレッドへのイベント ディスパッチを最小限に抑える

前回の投稿では、一般的なディスプレイが 1 秒間に 60 回画面を更新する仕組みと、スムーズなアニメーションを実現するためにそのペースに合わせる必要がある仕組みについて説明しました。入力の場合、一般的なタッチ スクリーン デバイスは 1 秒あたり 60 ~ 120 回、一般的なマウスでは 1 秒あたり 100 回のタッチイベントを配信します。入力イベントの忠実度が、画面の更新頻度よりも高い。

touchmove などの連続イベントが 1 秒あたり 120 回メインスレッドに送信された場合、画面の更新速度と比較して、ヒットテストと JavaScript の実行が過剰にトリガーされる可能性があります。

フィルタ未適用のイベント
図 7: フレーム タイムラインにイベントが殺到してページのジャンクが発生している

メインスレッドへの過剰な呼び出しを最小限に抑えるために、Chrome は連続するイベント(wheelmousewheelmousemovepointermovetouchmove など)を統合し、次の requestAnimationFrame の直前までディスパッチを遅らせます。

統合イベント
図 8: 前と同じタイムラインですが、イベントが統合され遅延しています

keydownkeyupmouseupmousedowntouchstarttouchend などの離散イベントは、すぐにディスパッチされます。

getCoalescedEvents を使用してフレーム内イベントを取得する

ほとんどのウェブ アプリケーションでは、統合イベントで十分に優れたユーザー エクスペリエンスを提供できます。ただし、描画アプリケーションなどのものを構築し、touchmove 座標に基づいてパスを配置する場合は、滑らかな線を描画するために中間の座標が失われる可能性があります。その場合は、ポインタ イベントの getCoalescedEvents メソッドを使用して、統合されたイベントに関する情報を取得できます。

getCoalescedEvents
図 9: スムーズなタップ ジェスチャーのパス(左)、統合された限定パス(右)
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 RussellPaul IrishMeggin KearneyEric BidelmanMathias BynensAddy OsmaniKinuko YasudaNasko Oskov、Charlie Reis など)に心より感謝いたします。

このシリーズはいかがでしたか?今後の記事についてご質問やご提案がございましたら、以下のコメント欄または Twitter の @kosamari までお寄せください。