Service Worker を Google 検索に対応

出荷された製品、影響の測定方法、トレードオフに関するストーリー。

背景

Google であらゆるトピックを検索すると、意味のある関連性の高い検索結果が一目でわかるページが表示されます。実は、この検索結果ページが、特定の状況下では、Service Worker という強力なウェブ テクノロジーによって提供されていることに気づかなかったかもしれません。

パフォーマンスに悪影響を与えることなく Service Worker のサポートを Google 検索にロールアウトするには、複数のチームで作業する何十人ものエンジニアが必要でした。これは、何が出荷され、パフォーマンスがどのように測定され、どのようなトレードオフが行われたかについてのストーリーです。

Service Worker を調べる主な理由

ウェブアプリに Service Worker を追加する場合も、サイトのアーキテクチャを変更する場合と同様に、明確な目標を設定して行う必要があります。Google 検索チームにとって、Service Worker の追加が検討に値する主な理由はいくつかありました。

検索結果のキャッシュ保存の制限

Google 検索チームは、ユーザーが同じ語句を短期間に複数回検索するのが一般的であることを特定しました。検索チームは、同じ結果になる可能性の高い結果を取得するためだけに新しいバックエンド リクエストをトリガーするのではなく、キャッシュを利用して、これらの繰り返しリクエストをローカルで処理したいと考えました。

鮮度の重要性は無視できません。ユーザーが同じ語句を繰り返し検索するのは、進化するトピックであり、最新の結果を期待することもあるためです。Service Worker を使用すると、検索チームはきめ細かいロジックを実装してローカルにキャッシュされた検索結果の存続期間を制御し、ユーザーに最適な速度と鮮度のバランスを取ることができます。

有意義なオフライン エクスペリエンス

また、Google 検索チームは、有意義なオフライン エクスペリエンスを提供したいと考えていました。ユーザーは、トピックについて調べたいとき、アクティブなインターネット接続を気にせずに、Google 検索ページに移動して検索を始めたいと思っています。

Service Worker がいない場合、オフラインで Google 検索ページにアクセスしようとすると、ブラウザの標準的なネットワーク エラー ページが表示されるだけで、ユーザーは接続が返された後にもう一度アクセスする必要があります。Service Worker を使用すると、カスタムのオフライン HTML レスポンスを返し、ユーザーが検索クエリをすぐに入力できるようにすることができます。

バックグラウンドでの再試行インターフェースのスクリーンショット。

インターネットに接続するまで結果は表示されませんが、Service Worker は検索を延期し、デバイスがバックグラウンド同期 API を使用してオンラインに戻ったらすぐに Google のサーバーに送信します。

よりスマートな JavaScript キャッシュとサービング

もう 1 つの目的は、検索結果ページのさまざまなタイプの機能を支えるモジュール化された JavaScript コードのキャッシュと読み込みを最適化することでした。JavaScript のバンドルには、Service Worker が関与しない場合にも有効ないくつかの利点があるため、Google 検索チームは単純にバンドルを完全に停止するとは考えていませんでした。

検索チームは、実行時に JavaScript のきめ細かいチャンクのバージョン管理とキャッシュを行う Service Worker の機能を使用することで、キャッシュ チャーンの量を減らし、将来的に再利用される JavaScript を効率的にキャッシュに保存できると考えました。Service Worker 内のロジックは、複数の JavaScript モジュールを含むバンドルへの送信 HTTP リクエストを分析し、ローカルにキャッシュされた複数のモジュールをつなぎ合わせることで処理できます。可能であれば、実質的に「分離」します。これにより、ユーザーの帯域幅が節約され、全体的な応答性が向上します。

Service Worker から提供されるキャッシュに保存された JavaScript を使用することには、パフォーマンス上の利点もあります。Chrome では、その JavaScript の解析されたバイトコード表現が保存され、再利用されるため、ページで JavaScript を実行するために実行時に行う必要がある作業が少なくなります。

課題と解決策

ここでは、チームが掲げた目標を達成するために克服する必要があるハードルをいくつか紹介します。これらの課題の一部は Google 検索に固有のものですが、その多くは Service Worker の導入を検討中のさまざまなサイトに当てはまります。

問題: Service Worker のオーバーヘッド

最大の課題であり、Google 検索で Service Worker を起動する際の真の阻害要因は、ユーザーが認識するレイテンシを増加させる可能性のある動作をしないようにすることでした。Google 検索ではパフォーマンスを非常に重視しています。過去には、特定のユーザーにおいて数十ミリ秒のレイテンシが増加した場合、新機能のリリースはブロックされていました。

チームが初期のテストでパフォーマンス データの収集を開始したところ、問題があることが明らかになりました。検索結果ページのナビゲーション リクエストに応答して返される HTML は動的であり、検索のウェブサーバーで実行する必要があるロジックによって大きく異なります。現在、Service Worker がこのロジックを複製し、キャッシュされた HTML をすぐに返す方法はありません。可能な最善の方法は、ナビゲーション リクエストをバックエンド ウェブサーバーに渡すことです。この場合、ネットワーク リクエストが必要になります。

Service Worker がない場合、このネットワーク リクエストはユーザー ナビゲーションの直後に発生します。Service Worker が登録されたら、フェッチ ハンドラがネットワークにアクセスする以外の処理を行う可能性がない場合でも、常に Service Worker を起動し、fetch イベント ハンドラを実行する機会を与える必要があります。Service Worker コードの起動と実行にかかる時間は、すべてのナビゲーションに追加する純粋なオーバーヘッドです。

SW 起動がナビゲーション リクエストをブロックしているイラスト。

このため、Service Worker の実装ではレイテンシが大きくなり、他のメリットを正当化できません。さらに、実際のデバイスで Service Worker の起動時間を測定したところ、起動時間の分布には幅があり、一部のローエンドのモバイル デバイスでは、Service Worker の起動に結果ページの HTML に対するネットワーク リクエストにかかる時間とほぼ同じ時間がかかることがわかりました。

解決策: ナビゲーションのプリロードを使用する

Google 検索チームが Service Worker のリリースに先駆けて前進するために役立った、最も重要な機能は、ナビゲーションのプリロードです。ナビゲーション リクエストを処理するためにネットワークからのレスポンスを使用する Service Worker の場合、ナビゲーション プリロードの使用は、パフォーマンス面で重要なメリットです。これにより、Service Worker の起動時にブラウザがナビゲーション リクエストをすぐに開始できるようになります。

ナビゲーション リクエストと並行して行われる SW の起動を示すイラスト。

Service Worker の起動にかかる時間が、ネットワークからレスポンスを受け取るのにかかる時間より短くない限り、Service Worker によるレイテンシのオーバーヘッドは発生しません。

また、検索チームは、Service Worker の起動時間がナビゲーション リクエストを超える可能性があるローエンド モバイル デバイスで Service Worker を使用しないようにする必要がありました。「ローエンド」デバイスの構成には厳格なルールがないため、デバイスにインストールされている合計 RAM を確認するというヒューリスティックを考案しました。2 GB 未満のメモリは、Service Worker の起動時間が許容できないローエンド デバイスのカテゴリに該当します。

後で使用するためにキャッシュに保存されるリソース全体が数メガバイトに達する可能性があるため、使用可能な保存容量も考慮する必要があります。Google 検索ページでは、navigator.storage インターフェースを使用することで、データをキャッシュに保存しようとしたときに保存容量の不足が原因で失敗するリスクがあるかどうかを事前に確認できます。

これにより、Service Worker を使用するかどうかを判断するための複数の基準が検索チームに残されました。ユーザーがナビゲーションのプリロードをサポートし、2 GB 以上の RAM と十分な空き容量があるブラウザを使用して Google 検索ページにアクセスした場合、Service Worker が登録されます。この条件を満たしていないブラウザやデバイスでは Service Worker が使用されることはありませんが、これまでと同じ Google 検索エクスペリエンスが引き続き表示されます。

この選択的登録の副次的なメリットの一つは、より小さく効率的な Service Worker を出荷できることです。最新のブラウザを対象に Service Worker コードを実行することで、古いブラウザのトランスパイルとポリフィルのオーバーヘッドがなくなります。これにより、Service Worker の実装の合計サイズから、圧縮されていない JavaScript コードが約 8 KB 削減されました。

問題: Service Worker のスコープ

検索チームが十分なレイテンシ テストを実施し、ナビゲーション プリロードを使用することで、Service Worker を使用するための実行可能でレイテンシ ニュートラルな方法が提供されることを確信した後、いくつかの実用的な問題に直面し始めました。そのうちの 1 つは、Service Worker のスコープ ルールに関係しています。Service Worker のスコープにより、Service Worker が制御できる可能性のあるページが決まります。

スコープは URL パスの接頭辞に基づいて機能します。単一のウェブアプリをホストするドメインの場合、これは問題にはなりません。通常は、最大スコープ / の Service Worker を使用するだけで、ドメイン内のあらゆるページを制御する可能性があるためです。しかし、Google 検索の URL 構造は少し複雑です。

Service Worker に / の最大スコープを与えると、www.google.com(または同等のリージョン)でホストされているあらゆるページを制御できるようになり、そのドメインには Google 検索とは無関係の URL が存在します。より合理的で制限の厳しいスコープは /search です。少なくとも、検索結果にまったく関係のない URL を除外します。

残念ながら、/search URL パスであっても、さまざまなタイプの Google 検索結果間で共有され、表示される検索結果の種類は URL クエリ パラメータによって決まります。一部のフレーバーは、従来のウェブ検索結果ページとはまったく異なるコードベースを使用しています。たとえば、画像検索とショッピング検索はどちらも、異なるクエリ パラメータを使用して /search URL パスで処理されますが、どちらのインターフェースも独自の Service Worker エクスペリエンスを(まだ)提供する準備が整っていません。

ソリューション: ディスパッチとルーティングのフレームワークを作成する

Service Worker のスコープを決定するために URL パス プレフィックスよりも強力な手段を可能にした提案もありますが、Google 検索チームは、制御するページのサブセットに対して何もしない Service Worker をデプロイしていました。

この問題を回避するために、Google 検索チームは独自のディスパッチおよびルーティング フレームワークを構築しました。このフレームワークでは、クライアント ページのクエリ パラメータなどの条件を確認し、それを使用してどのコードパスをたどるかを決定できます。ルールをハードコードするのではなく、URL 空間を共有するチームが、実装を決定した場合に、画像検索やショッピング検索など、URL 空間を共有するチームが独自の Service Worker ロジックを追加できるように構築されています。

問題: パーソナライズされた結果と指標

ユーザーは Google アカウントを使用して Google 検索にログインでき、検索結果のエクスペリエンスは特定のアカウント データに基づいてカスタマイズされる場合があります。ログイン ユーザーは、広くサポートされている老舗のブラウザ Cookie で識別されます。

ただし、ブラウザ Cookie を使用するデメリットの一つは、Service Worker 内では公開されず、ユーザーのログアウトやアカウントの切り替えによって値が変更されていないことを確認する方法が自動的にないことです。(Service Worker で Cookie にアクセスできるようにする作業が進行中ですが、この記事の執筆時点でこのアプローチは試験運用版であり、広くサポートされていません)。

Service Worker で現在ログインしているユーザーのビューと、Google 検索ウェブ インターフェースにログインしている実際のユーザーが一致しない場合、検索結果が正しくパーソナライズされなかったり、指標やロギングのアトリビューションが誤りたりする可能性があります。これらの障害シナリオはいずれも、Google 検索チームにとって深刻な問題です。

解決策: postMessage を使用して Cookie を送信する

Google 検索チームは、試験運用版 API がリリースされて Service Worker 内でブラウザの Cookie に直接アクセスできるのを待たずに、ストップギャップ ソリューションを採用しました。Service Worker によって制御されるページが読み込まれるたびに、そのページが関連する Cookie を読み取り、postMessage() を使用して Service Worker に送信します。

Service Worker は、現在の Cookie 値を期待値と照合し、不一致がある場合は、ストレージからユーザー固有のデータを削除し、不適切なパーソナライズを行わずに検索結果ページを再読み込みします。

ベースラインにリセットするために Service Worker が行う具体的な手順は、Google 検索の要件に固有のものですが、ブラウザの Cookie をキーとするパーソナライズ データを扱う他のデベロッパーにとっても、同じ一般的なアプローチが役立つ可能性があります。

問題: 実験とダイナミズム

前述したように、Google 検索チームは本番環境でのテストと、新しいコードや機能をデフォルトで有効にする前に現実世界でその効果をテストすることに大きく依存しています。ユーザーの試験運用版のオプトインとオプトアウトにはバックエンド サーバーとの通信が必要になることが多いため、キャッシュに保存されたデータに大きく依存する静的 Service Worker では、この作業はやや難しい場合があります。

解決策: 動的に生成される Service Worker スクリプト

チームが採用した解決策は、事前に生成される単一の静的な Service Worker スクリプトではなく、動的に生成される Service Worker スクリプトを使用して、ウェブサーバーが個々のユーザーごとにカスタマイズすることです。Service Worker の動作やネットワーク リクエスト全般に影響する可能性のあるテストに関する情報は、このカスタマイズされた Service Worker スクリプトに直接含まれています。ユーザーのアクティブなエクスペリエンスのセットを変更するには、ブラウザの Cookie などの従来の手法を組み合わせて、登録済みの Service Worker URL で更新されたコードを提供します。

また、動的に生成される Service Worker スクリプトを使用すると、回避する必要がある致命的なバグが Service Worker の実装で発生した万が一のときに、回避策を簡単に提供できます。動的サーバー ワーカーのレスポンスは NoOps の実装であり、現在のユーザーの一部または全部の Service Worker を効果的に無効にできます。

問題: 更新の調整

実際の Service Worker のデプロイで直面する最も困難な課題の 1 つは、ネットワークの使用を避けてキャッシュを優先する一方で、既存のユーザーが本番環境にデプロイされた直後に重要な更新と変更を確実に取得できるようにする、という合理的なトレードオフを考案することです。適切なバランスは、次のようなさまざまな要因に左右されます。

  • ウェブアプリが、ユーザーが新しいページに移動することなく無期限に開いたままの、存続期間が長いシングルページ アプリかどうか。
  • バックエンド ウェブサーバーの更新のデプロイ頻度。
  • 平均的なユーザーが少し古いバージョンのウェブアプリの使用を許容するか、鮮度が最優先事項か。

Service Worker をテストしながら、Google 検索チームは、スケジュールされたバックエンドの更新を多数実施しながらテストを継続し、指標とユーザー エクスペリエンスが、最終的にはリピーターに表示されるものとより近い状態になるようにしました。

解決策: 鮮度とキャッシュ使用率のバランスを取る

Google 検索チームは、さまざまな構成オプションをテストした結果、以下の設定によって鮮度とキャッシュ使用率の適切なバランスが得られることがわかりました。

Service Worker スクリプトの URL は、Cache-Control: private, max-age=1500(1,500 秒(25 分))レスポンス ヘッダーとともに提供され、updateViaCache を「all」に設定して登録され、ヘッダーが適用されます。Google 検索のウェブ バックエンドは、グローバルに分散した大規模なサーバーセットであり、可能な限り 100% に近い稼働時間が求められます。Service Worker スクリプトの内容に影響する変更のデプロイは、ローリング方式で行われます。

ユーザーが更新されたバックエンドにヒットした後、すぐに別のページに移動し、更新された Service Worker をまだ受け取っていないバックエンドに到達すると、バージョン間が何度もフリップフロップすることになります。したがって、最後のチェックから 25 分経過している場合にのみ、更新されたスクリプトのチェックのみを行うようにブラウザに指示しても、大きなデメリットはありません。この動作にオプトインすることの利点は、Service Worker スクリプトを動的に生成するエンドポイントが受信するトラフィックが大幅に減少することです。

さらに、Service Worker スクリプトの HTTP レスポンスに ETag ヘッダーが設定され、25 分経過後に更新チェックが行われると、その間に Service Worker が更新されていない場合にサーバーは HTTP 304 レスポンスで効率的に応答できます。

Google 検索ウェブアプリ内の一部の操作では、単一ページのアプリ形式のナビゲーション(History API 経由)を使用しますが、Google 検索の大部分は「実際の」ナビゲーションを使用する従来のウェブアプリです。これは、Service Worker の更新ライフサイクルを高速化する 2 つのオプション(clients.claim()skipWaiting())を使用することが効果的であるとチームが判断したときに関係します。通常、Google 検索のインターフェースをクリックすると、新しい HTML ドキュメントに移動します。skipWaiting を呼び出すと、更新された Service Worker は、インストール後すぐに新しいナビゲーション リクエストを処理できるようになります。同様に、clients.claim() を呼び出すと、Service Worker のアクティベーション後、更新された Service Worker は、制御されていない開いている Google 検索ページの制御を開始できます。

Google 検索が採用したアプローチは、必ずしもすべての人に適したソリューションというわけではありません。最適な方法が見つかるまで、提供オプションのさまざまな組み合わせを慎重に A/B テストした結果です。更新をより迅速にデプロイできるバックエンド インフラストラクチャを持つデベロッパーは、HTTP キャッシュを常に無視して、ブラウザが更新された Service Worker スクリプトをできるだけ頻繁に確認することをおすすめします。ユーザーが長期間開いたままになる単一ページのアプリを作成する場合、skipWaiting() の使用はおそらく適切な選択肢ではありません。存続期間が長いクライアントが存在するときに新しい Service Worker をアクティブにすると、キャッシュの不整合が発生するリスクが発生します。

重要ポイント

デフォルトでは、Service Worker はパフォーマンスに依存しない

ウェブアプリに Service Worker を追加すると、ウェブアプリがリクエストに対するレスポンスを受け取る前に、読み込んで実行する必要がある JavaScript を追加します。これらのレスポンスがネットワークではなくローカル キャッシュから送られてくる場合、通常、Service Worker の実行にかかるオーバーヘッドは、キャッシュ優先によるパフォーマンス向上に比べるとごくわずかです。ただし、ナビゲーション リクエストを処理するときに Service Worker が常にネットワークを参照する必要があることがわかっている場合は、ナビゲーション プリロードを使用することがパフォーマンスの向上に大きく影響します。

Service Worker は(現時点では)段階的な機能強化

Service Worker のサポート状況は、1 年前よりもはるかに明るくなっています。最新のブラウザはすべて、少なくとも一部 Service Worker のサポートを備えていますが、残念ながら、バックグラウンド同期やナビゲーションのプリロードなど、一部の高度な Service Worker 機能は一般的にロールアウトされていません。それでも、必要であることがわかっている特定の機能のサブセットの特徴チェックを行い、それらが存在する場合にのみ Service Worker を登録するというのが、妥当なアプローチです。

同様に、実際に実験を行い、Service Worker のオーバーヘッドが増加し、ローエンド デバイスのパフォーマンスが低下することがわかっている場合は、このようなシナリオでも Service Worker の登録を避けることができます。

Service Worker は、すべての前提条件が満たされ、Service Worker がユーザー エクスペリエンスと全体的な読み込みパフォーマンスに改善を加えた場合に、ウェブアプリに追加されるプログレッシブ エンハンスメントとして引き続き扱う必要があります。

すべてを数値で測る

Service Worker の提供がユーザー エクスペリエンスに好ましい影響または悪影響を及ぼしたかどうかを判断する唯一の方法は、結果をテストして測定することです。

意味のある測定の詳細な設定方法は、使用している分析プロバイダと、デプロイ設定で通常どのようにテストを行うかによって異なります。Google アナリティクスを使用して指標を収集する 1 つのアプローチについては、Google I/O ウェブアプリでの Service Worker の使用経験に基づくこちらの事例紹介で詳しく説明しています。

目標以外

ウェブ開発コミュニティでは、多くの人が Service Worker をプログレッシブ ウェブアプリに関連付けていましたが、「Google 検索 PWA」の構築はチームの最初の目標ではありませんでした。Google 検索ウェブアプリは現在、ウェブアプリ マニフェストを介してメタデータを提供していません。また、ユーザーにホーム画面に追加のフローを実行するよう促すこともありません。検索チームは現在、ユーザーが Google 検索の従来のエントリ ポイント経由でウェブアプリにアクセスしていることに満足しています。

Google 検索のウェブ エクスペリエンスを、インストール済みのアプリと同等のものに変えようとするのではなく、最初のロールアウトでは既存のウェブサイトを段階的に強化することでした。

謝辞

Service Worker の実装に取り組み、この記事の執筆に役立った背景資料を提供してくれた Google 検索ウェブ開発チーム全体に感謝します。特に Philippe Golle 氏、Rajesh Jagannathan 氏、R. Samuel Klatchko、Andy Martone、Leonardo Peña、Rachel Shearer、Greg Terrono、Clay Woolam です

更新(2021 年 10 月): この記事を最初に公開してから、Google 検索チームは現在の Service Worker アーキテクチャの利点とトレードオフを再評価しました。上記の Service Worker は廃止されます。Google 検索のウェブ インフラストラクチャが進化するにつれて、Service Worker の設計を再検討することもあります。