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

Mariko Kosaka

コンポジタに入力されます

全 4 部構成のブログシリーズの最後となります。今回は Chrome の内部を見て、コードの処理によってウェブサイトを表示する方法について解説します。前回の投稿では、レンダリング プロセスを確認し、コンポジターについて学習しました。この投稿では、コンポジタがユーザー入力を受け取ったときにスムーズな操作を可能にする仕組みについて説明します。

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

「入力イベント」と聞くと、テキストボックスへの入力またはマウスクリックのみを思い浮かべるかもしれませんが、ブラウザの観点からは、入力はユーザーのあらゆるジェスチャーを意味します。マウスホイールのスクロールは入力イベントであり、タップまたはマウスオーバーも入力イベントです。

画面のタップなどのユーザー ジェスチャーが発生したとき、最初にそのジェスチャーを受け取るのはブラウザ プロセスです。ただし、タブ内のコンテンツはレンダラ プロセスによって処理されるため、ブラウザ プロセスはそのジェスチャーが発生した場所のみを認識します。したがって、ブラウザ プロセスはイベントタイプ(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 がウェブサイトの監査を実施して、何が適切か、何を改善すべきかに関するレポートを提供するツールです。監査のリストを確認することで、ブラウザがどのようなことに注意を払っているかを把握できます。

パフォーマンスの測定方法

パフォーマンスの微調整はサイトによって異なる可能性があるため、サイトのパフォーマンスを測定して、サイトに最適な方法を決定することが重要です。Chrome DevTools チームには、サイトのパフォーマンスを測定する方法に関するチュートリアルがいくつか用意されています。

サイトに機能ポリシーを追加する

さらに、機能ポリシーは新しいウェブ プラットフォーム機能で、プロジェクトをビルドする際のガードレールとして利用できます。機能ポリシーをオンにすると、アプリの特定の動作が保証され、ミスを防ぐことができます。 たとえば、アプリが解析をブロックしないようにするには、同期スクリプト ポリシーでアプリを実行します。sync-script: 'none' を有効にすると、パーサーをブロックする JavaScript は実行されなくなります。これにより、どのコードもパーサーをブロックする必要がなく、ブラウザはパーサーの一時停止を気にする必要がなくなります。

まとめ

ありがとう

ウェブサイトを作り始めたとき、私はコードをどのように記述するか、何が生産性の向上に役立つかということしか考えていませんでした。これらは重要ですが、記述したコードをブラウザがどのように受け取るかについても考える必要があります。最新のブラウザは、優れたウェブ エクスペリエンスをユーザーに提供する方法に投資を続けてきました。コードを整理してブラウザに配慮すると、ユーザー エクスペリエンスが向上します。ブラウザへの対応にぜひご協力ください。

本シリーズの初期の下書きをレビューしていただいた皆様に心より感謝いたします。以下はその一例です。Alex RussellPaul IrishMeggin KearneyEric BidelmanMathias BynensAddy OsmaniOd.ovisOsmani、Osmani、Osmani、Osmani 氏

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