Chrome のパフォーマンスを向上するという点では、「これで十分」ということは決してありません。今回の速さと好奇心の投稿では、Android 版 Chrome の起動時間を 20% 以上高速化した方法に迫ります。これは、起動時にタブのインタラクティブなフリーズドライ プレビューを表示することで実現しました。以降では、スクリーンショットでは何が不十分なのか、なぜタブをフリーズドライすることが優れたブラウザにつながるのかについて説明します。
ウェブ コンテンツのレンダリングにはときに重い計算処理が必要で、ネイティブ アプリケーションよりも遅く感じられることがあります。ネットワークから動的にリソースを読み込んだり、JavaScript を実行したり、CSS やフォントなどをレンダリングしたりするには、たくさんの作業が必要です。この問題はモバイル デバイスで特に顕著で、デバイスのメモリが制約となり、Chrome が一度に少数のウェブページしか読み込めないこともよくあります。
ここから生じるのが、必要に迫られた場合(タブ スイッチャーのような一時的な UI や、たくさんのウォームアップ作業が行われる起動時など)に、ウェブ コンテンツを軽量に表現する方法はないかという疑問です。これを行う標準的な手法はスクリーンショットです。スクリーンショットは見た目を正確に表現できるので、ユーザーは何が開いているかを一目で理解できます。しかし、スクリーンショットは最後に表示したものしか表現できず、完全に静的なので、ウェブページよりも制限が強くなります。
この一時的なウェブ コンテンツのイメージがより便利でインタラクティブになり、本物のページが準備できるまで待つ間に利用できるならどうでしょうか。
Android 版 Chrome アプリのコールド スタートは高価で、起動してからウェブページの描画を始めるまでの時間(First Contentful Paint / FCP)の中央値は 3.4 秒です。ページの HTML、CSS、JS、フォントを処理するにはたくさんの作業が必要なので、他のアプリと比べると遅く感じられるかもしれません。
しかし、起動時にインタラクティブなページのスナップショットを表示できたとしたらどうでしょうか。
このスナップショットを、フリーズドライ タブと呼びます。これは実際のウェブページからさまざまな機能を取り除いたものですが、十分な内容とインタラクティブ性を持ち合わせているため、静的なスクリーンショットよりも有用です。スクリーンショットに欠けていた重要な要素は、リンクを開いたり、ページのコンテンツをスクロールしてビューポート外の内容(iframe も含む)を見たりする機能です。
フリーズドライ タブは、このすべての機能に加えて、ほかの機能も実現できます。実際のウェブページよりも速く起動し、完全なページが準備できるまでの間にもコンテンツを利用できるように、十分な機能を提供します。ページが読み込まれると、自動的かつシームレスにそのページに切り替わります。
テストの結果、フリーズドライ タブを使うことで、起動してからページのすべてのコンテンツを描画するまでの時間の中央値が 2.8 秒まで短縮できました(通常の描画を始める場合に比べて最大 20% 高速)。すべてのコンテンツが表示され、ほとんどの場合、レイアウトのずれも起こらないので、一層速く感じられます。
フリーズドライ タブによる起動時間の分散の変化
すべての統計情報の出典 : Chrome クライアントから匿名で集計した実データ [1]
ウェブページをフリーズドライするため、ページの視覚的な状態を一連のベクター グラフィックとしてキャプチャします。その際に、すべてのハイパーリンクも取得します。後に、そのベクター グラフィックを単純にラスタライズし、軽量な形で「再構築」(再生)します。これにより、完全なウェブページ(ビューポートの外側のコンテンツも含む)を表示するレンダリング コストを省きつつ、ハイパーリンクをサポートできます。
この形式には、スクリーンショットよりも多くのメリットがありますが、ウェブページのすべての機能が利用できるわけではありません。そのため、私たちは、実際のページを読み込むには少し時間がかかるときに、スクリーンショットよりもインタラクティブな表示をしたい場合は、この形式で一時的な表示をするのが最適な方法であると考えています。
*Android P を実行する Pixel 2 XL をエミュレーションした推定値。
1 ユーティリティ プロセスは 30 MB のオーバーヘッド(最大平均 10 MB のコンテンツと 20 MB のビットマップ)
この技術を構築できたことは、興味深くやりがいのある経験でした。特に難しいのは、iframe のコンテンツを集約すること、サブフレームのスクロールをサポートすること、すべてのジオメトリを扱うことです。
しかし、最も興味深い挑戦はパフォーマンスでした。
キャプチャ
ページをキャプチャするときにコンテンツを保存するのは単純な作業です。CSS でスタイル設定した DOM のジオメトリは簡単にベクター グラフィックに変換でき、ベクター グラフィックは小さくて、保存が簡単です。
ページのイメージをこの形式で保存するのも単純ですが、高解像度のイメージはサイズが大きくて(0.1~10 MB)圧縮も O(100 ms) と遅く、MB 単位のメモリのオーバーヘッドもかかります。そのため、イメージをそのままデフォルトのエンコードで保存するのが通例ですが、イメージが大きくなると対応できない場合があります。
フォントは、内包する各グリフの描画方法を記述したファイルです。中国語のように文字の種類が多い言語や、絵文字のようにイメージで構成されている場合、フォント ファイルのサイズは特に大きくなります。英語フォントは 1 つあたり 100 kB 程度のものが多いですが、絵文字のフォントは簡単に数 MB に達します。ページには複数のフォントが埋め込まれていることが多く、そういったフォントはローカル システムに保存されないため、キャプチャするデータの一部として保存しなければなりません。初期テストでは、見た目を完全に再現できるように、ページで使われているすべてのフォントを保存しようとしました。しかし、この方法で保存すると、ページによっては 100 MB ほどのサイズになることがありました。パフォーマンスとストレージの観点から考えて、これは受け入れられません。
この難題を克服するため、フォントのサブセット化に注目しました。サブセット化とは、フォント ファイルからすべての未使用グリフを取り除くことです。これにより、ページに必要なフォントのみがデータに残ります。すると、100 MB だったページはわずか 400 kB(元のサイズの 1% 未満)になりました。
再生
もう 1 つの難題は、再生のパフォーマンスを妥当な範囲に保つことでした。ベクター グラフィックを表示するには、ラスタライズしてビットマップにしなければなりません。しかし、現在のスマートフォンの画面は 1 ピクセルあたり 32 ビットなので、コンテンツのビューポート 1 つでも容易に 10 MB を超えてしまいます。このビットマップのメモリ オーバーヘッドを減らすため、ユーザーがスクロールする際に動的にビットマップを生成するようにしました。
もう少し詳しく説明しましょう。ページのコンテンツは、ビューポートよりも小さなタイルに分割します。そして、ビューポートに現在含まれるすべてのタイルのビットマップを生成するとともに、スムーズなスクロールを実現するため、ビューポートの周囲にあるタイルをプリフェッチします。ビューポート外のビットマップを実際に表示するまで圧縮する実験をしたところ、10 MB からわずか 100 kB ほどまでメモリを節約できる可能性があることがわかりました。しかし、さらにパフォーマンス データを収集したところ、圧縮によって CPU に追加のオーバーヘッドがかかるため、ブラウザのジャンクや [FID] などが大幅に増加することがわかりました。そこで、この動作は削除し、タイルを小さくしてビューポート外のビットマップを積極的に破棄できるようにしました。
フリーズドライ タブは、スクリーンショットに替わるものとして魅力的な選択肢です。一時的な表示や、すぐにウェブ コンテンツを準備できず、それが利用できるようになるまで待つ時間が長い場合には特に有効です。また、スクリーンショットよりも再現性に優れているほか、リンクやスクロールなど、ウェブページと同じように動作する便利なユーザー操作も可能です。
現在、Android 版の Chrome で使われているフリーズドライ タブによって、コールド スタートで 20% という体感可能な高速化が実現されています。この技術を他の場所で使うことも検討しています。