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

Mariko Kosaka

コンポジタへの入力

これは、Chrome の内部を探る 4 部構成のブログシリーズの最後です。ウェブサイトを表示するためにコードを処理する方法について説明します。前回の投稿では、レンダリング プロセスとコンポジタについて学びました。この記事では、コンポジタがユーザー入力の受信時にスムーズな操作を可能にする仕組みについて説明します。

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

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

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

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

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

図 2: ページレイヤの上にホバーしているビューポート

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

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

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 までお寄せください。