リリースされた内容、影響の測定方法、妥協点について説明します。
背景
Google でほぼすべてのトピックを検索すると、有意で関連性の高い結果がすぐに表示されます。おそらく気付いていないかもしれませんが、この検索結果ページは、特定の状況下では、サービス ワーカーと呼ばれる強力なウェブ技術によって提供されています。
パフォーマンスに悪影響を及ぼすことなく Google 検索の Service Worker サポートをリリースするには、複数のチームにまたがる数十人のエンジニアが必要でした。本書では、リリースされた内容、パフォーマンスの測定方法、トレードオフについて説明します。
サービス ワーカーを検討する主な理由
ウェブアプリにサービス ワーカーを追加する場合は、サイトのアーキテクチャを変更する場合と同様に、明確な目標を念頭に置いて行う必要があります。Google 検索チームがサービス ワーカーの追加を検討した理由はいくつかあります。
検索結果のキャッシュ保存の制限
Google 検索チームは、ユーザーが短時間に同じキーワードを複数回検索することが一般的であることを発見しました。検索チームは、同じ結果が得られる可能性が高いものを取得するためだけに新しいバックエンド リクエストをトリガーするのではなく、キャッシュを利用して、これらの繰り返しリクエストをローカルで処理することを望んでいました。
検索結果の更新頻度の重要性は軽視できません。ユーザーは、トピックが進展しているため、最新の検索結果が表示されることを期待して、同じキーワードを繰り返し検索することがあります。検索チームは、サービス ワーカーを使用してきめ細かいロジックを実装し、ローカルにキャッシュに保存された検索結果の存続期間を制御し、ユーザーにとって最適な速度と新しさのバランスを正確に実現できます。
有意義なオフライン エクスペリエンス
また、Google 検索チームは、有意義なオフライン エクスペリエンスを提供したいとも考えていました。ユーザーは、トピックについて調べたいときに、インターネット接続が有効かどうかを気にすることなく、Google 検索ページに直接アクセスして検索を開始したいと考えています。
サービス ワーカーがないと、オフラインで Google 検索ページにアクセスすると、ブラウザの標準のネットワーク エラー ページが表示されるだけです。ユーザーは、接続が復元されたらもう一度アクセスしてやり直す必要があります。サービス ワーカーを使用すると、カスタムのオフライン HTML レスポンスを提供して、ユーザーが検索クエリをすぐに入力できるようにできます。
インターネット接続が確立されるまで結果は得られませんが、サービス ワーカーを使用すると、検索を延期し、デバイスがオンラインに戻るとすぐに バックグラウンド同期 API を使用して Google のサーバーに送信できます。
よりスマートな JavaScript のキャッシュ保存と配信
もう 1 つの理由は、検索結果ページのさまざまな種類の機能を支えるモジュール化された JavaScript コードのキャッシュと読み込みを最適化することでした。JavaScript バンドルには、サービス ワーカーが関与していない場合に有用なメリットがいくつかあります。そのため、検索チームはバンドルを完全に停止することは望んでいません。
検索チームは、サービス ワーカーの機能を使用して、JavaScript のきめ細かいチャンクを実行時にバージョニングしてキャッシュに保存することで、キャッシュの更新頻度を減らし、今後再利用される JavaScript を効率的にキャッシュに保存できるのではないかと考えました。サービス ワーカー内のロジックは、複数の JavaScript モジュールを含むバンドルの送信 HTTP リクエストを分析し、ローカルにキャッシュに保存されている複数のモジュールを組み合わせてリクエストを処理します。可能であれば、実質的に「バンドルを解除」します。これにより、ユーザーの帯域幅を節約し、全体的な応答性を向上させることができます。
サービス ワーカーによって提供されるキャッシュに保存された JavaScript を使用すると、パフォーマンス上のメリットもあります。Chrome では、その JavaScript の解析されたバイトコード表現が保存され、再利用されるため、ページで JavaScript を実行するために実行時に必要な作業が減ります。
課題と解決策
チームが掲げた目標を達成するために克服する必要があったハードルをいくつか紹介します。これらの課題の一部は Google 検索に固有のものですが、その多くは、サービス ワーカーのデプロイを検討している可能性のある幅広いサイトに適用されます。
問題: サービス ワーカーのオーバーヘッド
Google 検索でサービス ワーカーをリリースする際の最大の課題であり、唯一の障害は、ユーザーが認識するレイテンシを増加させる可能性のある処理をサービス ワーカーが行わないようにすることです。Google 検索ではパフォーマンスを非常に重視しており、過去には、特定のユーザー群でレイテンシが数十ミリ秒でも増加する新機能のリリースをブロックしてきました。
チームが最初のテストでパフォーマンス データを収集し始めたとき、問題が発生することが明らかになりました。検索結果ページのナビゲーション リクエストに応答して返される HTML は動的であり、検索のウェブサーバーで実行する必要があるロジックに応じて大きく異なります。現在のところ、サービス ワーカーがこのロジックを複製してキャッシュに保存された HTML をすぐに返す方法はありません。最善の方法は、ナビゲーション リクエストをバックエンド ウェブサーバーに渡すことですが、この場合ネットワーク リクエストが必要になります。
サービス ワーカーがないと、このネットワーク リクエストはユーザーのナビゲーション直後に行われます。サービス ワーカーが登録されると、そのサービス ワーカーは常に起動され、fetch
イベント ハンドラを実行する機会が与えられます。たとえ、それらの取得ハンドラがネットワークにアクセスする以外に何も実行する可能性がないとしても、このようになります。サービス ワーカー コードの起動と実行にかかる時間は、すべてのナビゲーションに追加される純粋なオーバーヘッドです。
これにより、サービス ワーカーの実装は、他のメリットを正当化するには遅延が大きすぎます。また、実際のデバイスでのサービス ワーカーの起動時間を測定した結果、起動時間は幅広く分布しており、一部のローエンドのモバイル デバイスでは、サービス ワーカーの起動に要する時間が、結果ページの HTML のネットワーク リクエストに要する時間とほぼ同じだったこともわかりました。
解決策: ナビゲーションのプリロードを使用する
Google 検索チームがサービス ワーカーのリリースを進めることができた、最も重要な機能はナビゲーション プリロードです。ナビゲーション プリロードを使用することは、ナビゲーション リクエストを満たすためにネットワークからのレスポンスを使用しなければならないすべてのサービス ワーカーにとって、パフォーマンスを向上させるうえで重要な要素です。サービス ワーカーの起動と同時に、ナビゲーション リクエストをすぐに開始するようにブラウザにヒントを提供します。
サービス ワーカーの起動にかかる時間がネットワークからレスポンスを受信する時間より短い限り、サービス ワーカーによってレイテンシのオーバーヘッドが発生することはありません。
また、検索チームは、サービス ワーカーの起動時間がナビゲーション リクエストを超える可能性がある低価格のモバイル デバイスでサービス ワーカーを使用しないようにする必要がありました。「ローエンド」デバイスを構成する明確なルールがないため、デバイスにインストールされている合計 RAM を確認するヒューリスティクスが考案されました。2 GB 未満のメモリは低価格デバイスのカテゴリに分類され、サービス ワーカーの起動時間が許容されませんでした。
使用可能なストレージ容量も考慮する必要があります。今後使用するためにキャッシュに保存するリソースのセット全体は数メガバイトになる可能性があります。navigator.storage
インターフェースを使用すると、Google 検索ページで、データのキャッシュ保存がストレージ割り当て不足により失敗するリスクがあるかどうかを事前に把握できます。
これにより、検索チームは、サービス ワーカーを使用するかどうかを判断するために使用できる複数の条件を設定できました。ユーザーがナビゲーションのプリロードをサポートするブラウザを使用して Google 検索ページにアクセスし、RAM が 2 ギガバイト以上で、十分な空きストレージ容量がある場合、サービス ワーカーが登録されます。この条件を満たさないブラウザやデバイスでは、サービス ワーカーは使用されませんが、これまでどおり Google 検索を利用できます。
この選択的な登録の副次的なメリットとして、より小さく効率的なサービス ワーカーを配布できることが挙げられます。比較的新しいブラウザをターゲットにしてサービス ワーカー コードを実行すると、古いブラウザのトランスパイルとポリフィルのオーバーヘッドを排除できます。これにより、サービス ワーカーの実装の合計サイズから約 8 KB の非圧縮 JavaScript コードが削減されました。
問題: サービス ワーカーのスコープ
検索チームが十分なレイテンシ テストを実施し、ナビゲーション プリロードを使用すると、サービス ワーカーを使用するための実用的なレイテンシに影響しないパスが提供されることを確信した後、いくつかの実際の問題が浮上してきました。そのような問題の一つは、サービス ワーカーのスコープルールに関連しています。Service Worker のスコープによって、制御できるページが決まります。
スコープ設定は URL パスの接頭辞に基づいて行われます。単一のウェブアプリをホストするドメインの場合は、通常、最大スコープが /
の Service Worker のみを使用するため、ドメイン内の任意のページを制御できます。ただし、Google 検索の URL 構造は少し複雑です。
サービス ワーカーに最大スコープの /
が付与されている場合、www.google.com
(または地域に応じた同等の URL)でホストされているすべてのページを制御できるようになります。そのドメインには、Google 検索とは関係のない URL が含まれています。より合理的で制限の厳しいスコープは /search
です。これにより、少なくとも検索結果とまったく関係のない URL は除外されます。
残念ながら、その /search
URL パスも、Google 検索結果のさまざまな種類で共有されており、表示される検索結果の種類は URL クエリ パラメータによって決まります。これらのフレーバーの一部は、従来のウェブ検索結果ページとはまったく異なるコードベースを使用しています。たとえば、画像検索とショッピング検索はどちらも、異なるクエリ パラメータを使用して /search
URL パスで提供されますが、どちらのインターフェースも(まだ)独自のサービス ワーカー エクスペリエンスを提供できる状態ではありません。
解決策: ディスパッチとルーティングのフレームワークを作成する
URL パス接頭辞よりも強力な方法でサービス ワーカーのスコープを決定できる提案もありますが、Google 検索チームは、制御するページのサブセットに対して何も行わないサービス ワーカーをデプロイせざるを得ませんでした。
この問題を回避するため、Google 検索チームは、クライアント ページのクエリ パラメータなどの条件をチェックし、それらを使用してどの特定のコードパスを停止するかを決定するように構成できる、カスタム ディスパッチとルーティングのフレームワークを構築しました。このシステムは、ルールをハードコードするのではなく、柔軟性を持たせて構築されています。これにより、URL スペースを共有するチーム(画像検索やショッピング検索など)は、独自のサービス ワーカー ロジックを実装できます。
問題: パーソナライズされた結果と指標
ユーザーは Google アカウントを使用して Google 検索にログインできます。検索結果は、特定のアカウントデータに基づいてカスタマイズできます。ログインしたユーザーは、特定のブラウザ Cookie によって識別されます。これは、古くから広くサポートされている標準です。
ただし、ブラウザ Cookie を使用する場合の欠点の 1 つは、サービス ワーカー内で公開されないことです。値を自動的に検査して、ユーザーがログアウトしたりアカウントを切り替えたりしたために値が変更されていないことを確認する方法はありません。(サービス ワーカーに Cookie アクセスを導入するための取り組みは進行中ですが、この記事の執筆時点では、このアプローチは試験運用版であり、広くサポートされていません)。
サービス ワーカーが認識している現在ログイン中のユーザーと、Google 検索ウェブ インターフェースにログインしている実際のユーザーが一致しない場合、検索結果が誤ってパーソナライズされたり、指標やロギングが誤って割り当てられたりする可能性があります。これらの障害シナリオはいずれも、Google 検索チームにとって重大な問題となります。
解決策: postMessage を使用して Cookie を送信する
Google 検索チームは、試験運用版 API がリリースされてサービス ワーカー内でブラウザの Cookie に直接アクセスできるようになるのを待つのではなく、その場しのぎの解決策を採用しました。サービス ワーカーによって制御されるページが読み込まれるたびに、そのページは関連する Cookie を読み取り、postMessage()
を使用してサービス ワーカーに送信します。
サービス ワーカーは、現在の Cookie 値を想定される値と照らし合わせ、不一致がある場合は、ストレージからユーザー固有のデータをすべてパージし、誤ったパーソナライズをせずに検索結果ページを再読み込みします。
サービス ワーカーがベースラインにリセットするために行う具体的な手順は Google 検索の要件に固有のものですが、ブラウザ Cookie に基づいてパーソナライズされたデータを扱う他のデベロッパーにも、同じ一般的なアプローチが役立つ場合があります。
問題: テストと動的要素
前述のように、Google 検索チームは、本番環境でテストを実施し、新しいコードや機能の効果を実際の環境でテストしてから、デフォルトで有効にすることに大きく依存しています。キャッシュに保存されたデータに大きく依存する静的サービス ワーカーでは、ユーザーが試験運用版を有効または無効にするためにバックエンド サーバーと通信することが必要になるため、これは少し難しい場合があります。
ソリューション: 動的に生成されたサービス ワーカー スクリプト
チームが採用したソリューションは、事前に生成された単一の静的サービス ワーカー スクリプトではなく、動的に生成されたサービス ワーカー スクリプトを使用するというものでした。このスクリプトは、ウェブサーバーが個々のユーザーごとにカスタマイズします。サービス ワーカーの動作やネットワーク リクエスト全般に影響する可能性があるテストに関する情報は、このカスタマイズされたサービス ワーカー スクリプトに直接含まれています。ユーザーのアクティブなエクスペリエンスのセット変更は、ブラウザ Cookie などの従来の手法と、登録されたサービス ワーカー URL で更新されたコードを提供する手法の組み合わせによって行われます。
動的に生成された Service Worker スクリプトを使用すると、Service Worker の実装に致命的なバグがあり、回避する必要がある場合でも、エスケープ ハッチを簡単に提供できます。動的サーバー ワーカーのレスポンスは、no-op 実装で、現在のユーザーの一部またはすべてに対してサービス ワーカーを事実上無効にできます。
問題: 更新の調整
実際のサービス ワーカーのデプロイで直面する最も困難な課題の一つは、ネットワークを回避してキャッシュを使用することと、本番環境にデプロイされた直後に既存のユーザーに重要な更新と変更を確実に提供することの間で、妥当なトレードオフを設計することです。適切なバランスは、次のような多くの要因によって異なります。
- ウェブアプリが、ユーザーが新しいページに移動することなく、無期限に開いたままにする長寿のシングルページ アプリであるかどうか。
- バックエンド ウェブサーバーの更新のデプロイ頻度。
- 平均的なユーザーがウェブアプリの少し古いバージョンの使用を許容するかどうか、または新しさが最優先事項であるかどうか。
サービス ワーカーのテスト中、Google 検索チームは、指標とユーザー エクスペリエンスが、リピーターが実際に目にする内容に近づくように、スケジュールされた複数のバックエンド アップデートでテストを継続しました。
解決策: 更新頻度とキャッシュ使用率のバランスを取る
Google 検索チームは、さまざまな構成オプションをテストした結果、次の設定が更新頻度とキャッシュ使用率のバランスが取れていると判断しました。
サービス ワーカー スクリプトの URL は、Cache-Control: private, max-age=1500
(1,500 秒、25 分)レスポンス ヘッダーで提供され、updateViaCache が「all」に設定されて登録されているため、ヘッダーが確実に適用されます。Google 検索のウェブ バックエンドは、ご想像のとおり、グローバルに分散された大規模なサーバー セットであり、可能な限り 100% の稼働時間を必要とします。サービス ワーカー スクリプトのコンテンツに影響する変更は、ローリング方式でデプロイされます。
ユーザーが更新されたバックエンドにアクセスし、すぐに別のページに移動して、更新されたサービス ワーカーをまだ受け取っていないバックエンドにアクセスすると、バージョンが何度も切り替わってしまいます。したがって、最後のチェックから 25 分経過した場合にのみ更新されたスクリプトをチェックするようにブラウザに指示しても、大きなデメリットはありません。この動作を有効にすると、サービス ワーカー スクリプトを動的に生成するエンドポイントが受信するトラフィックが大幅に削減されます。
また、サービス ワーカー スクリプトの HTTP レスポンスに ETag ヘッダーが設定されているため、25 分経過後に更新チェックが行われた場合、その間にデプロイされたサービス ワーカーに更新がない場合、サーバーは HTTP 304 レスポンスで効率的に応答できます。
Google 検索ウェブアプリ内の一部の操作では、シングルページ アプリ スタイルのナビゲーション(History API 経由)が使用されますが、ほとんどの場合、Google 検索は「実際の」ナビゲーションを使用する従来のウェブアプリです。これは、サービス ワーカーの更新ライフサイクルを高速化する 2 つのオプション(clients.claim()
と skipWaiting()
)を使用するのが効果的であるとチームが判断した場合に役立ちます。通常、Google 検索のインターフェースをクリックすると、新しい HTML ドキュメントに移動します。skipWaiting
を呼び出すと、更新されたサービス ワーカーがインストール直後に新しいナビゲーション リクエストを処理できるようになります。同様に、clients.claim()
を呼び出すと、更新されたサービス ワーカーは、サービス ワーカーの有効化後に、制御されていない開いている Google 検索ページの制御を開始できます。
Google 検索が採用したアプローチは、必ずしもすべてのユーザーに適したソリューションではありません。これは、最適な方法を見つけるまで、さまざまな配信オプションの組み合わせを慎重に A/B テストした結果です。バックエンド インフラストラクチャで更新をより迅速にデプロイできるデベロッパーは、HTTP キャッシュを常に無視して、ブラウザが更新されたサービス ワーカー スクリプトをできるだけ頻繁にチェックすることをおすすめします。ユーザーが長時間開いたままにする可能性があるシングルページ アプリを構築する場合、skipWaiting()
の使用は適切ではありません。長時間のクライアントが存在するときに新しいサービス ワーカーを有効にすると、キャッシュの不整合が発生するリスクがあります。
重要ポイント
デフォルトでは、サービス ワーカーはパフォーマンスに影響しません
ウェブアプリにサービス ワーカーを追加すると、ウェブアプリがリクエストのレスポンスを取得する前に読み込みと実行が必要な JavaScript が追加されます。これらのレスポンスがネットワークではなくローカル キャッシュから取得される場合、通常、サービス ワーカーの実行のオーバーヘッドは、キャッシュファーストによるパフォーマンスの向上と比較して無視できるレベルです。ただし、サービス ワーカーがナビゲーション リクエストを処理する際に常にネットワークをコンサルトする必要がある場合は、ナビゲーション プリロードを使用するとパフォーマンスが大幅に向上します。
サービス ワーカーは(今でも)段階的な拡張機能です
サービス ワーカーのサポート状況は、1 年前と比べて大幅に改善されています。最新のブラウザでは、少なくとも一部の Service Worker のサポートが提供されていますが、残念ながら、バックグラウンド同期やナビゲーションのプリロードなど、一部の高度な Service Worker 機能は、すべてのブラウザでロールアウトされていません。必要とわかっている特定のサブセットの機能の機能チェックを行い、それらが存在する場合にのみサービス ワーカーを登録することは、引き続き合理的なアプローチです。
同様に、実際の環境でテストを実施し、低価格デバイスでサービス ワーカーのオーバーヘッドが原因でパフォーマンスが低下することを把握している場合は、そのようなシナリオでもサービス ワーカーを登録しないようにできます。
引き続き、サービス ワーカーをプログレッシブ エンハンスメントとして扱い、すべての前提条件が満たされ、サービス ワーカーがユーザー エクスペリエンスと全体的な読み込みパフォーマンスにプラスの効果をもたらす場合に、ウェブアプリに追加する必要があります。
すべてを数値で測る
サービス ワーカーのリリースがユーザー エクスペリエンスにプラスの影響を与えたか、マイナスの影響を与えたかを判断する唯一の方法は、テストして結果を測定することです。
有意な測定値を設定するための具体的な方法は、使用しているアナリティクス プロバイダと、デプロイ セットアップで通常実施しているテスト方法によって異なります。Google アナリティクスを使用して指標を収集する方法については、Google I/O ウェブアプリでのサービス ワーカーの使用に関するこちらのケーススタディをご覧ください。
目標とすべきでないこと
ウェブ開発コミュニティの多くのメンバーは、Service Worker をプログレッシブ ウェブアプリと関連付けていますが、チームの最初の目標は「Google 検索 PWA」の構築ではありませんでした。現在、Google 検索ウェブアプリは ウェブアプリ マニフェストを介してメタデータを提供しておらず、ユーザーにホーム画面に追加フローを案内することもありません。検索チームは現在、Google 検索の従来型のエントリ ポイントからウェブアプリにアクセスするユーザーに満足しています。
最初のリリースでは、Google 検索のウェブ エクスペリエンスをインストール済みのアプリに匹敵するものにしようとするのではなく、既存のウェブサイトを段階的に強化することに重点を置きました。
謝辞
Google 検索ウェブ開発チームの皆様には、Service Worker の実装にご尽力いただき、また、この記事の執筆に役立つ背景資料をお知らせいただき、ありがとうございました。特に、Philippe Golle、Rajesh Jagannathan、R. Samuel Klatchko、Andy Martone、Leonardo Peña、Rachel Shearer、Greg Terrono、Clay Woolam 氏
更新(2021 年 10 月): この記事の初回公開以降、Google 検索チームは現在のサービス ワーカー アーキテクチャのメリットとトレードオフを再評価しました。上記のサービス ワーカーは廃止されます。Google 検索のウェブ インフラストラクチャが進化するにつれて、チームは Service Worker の設計を見直す可能性があります。