リリースされた内容、影響の測定方法、トレードオフに関するストーリー。
公開日: 2019 年 6 月 20 日
Google でほぼすべてのトピックを検索すると、意味のある関連性の高い結果がすぐにわかるページが表示されます。この検索結果ページは、特定のシナリオでは、サービス ワーカーと呼ばれる強力なウェブ テクノロジーによって提供されます。
Google 検索でサービス ワーカーのサポートを導入し、パフォーマンスに悪影響を与えないようにするには、複数のチームに所属する数十人のエンジニアが協力して取り組む必要がありました。この記事では、出荷されたもの、パフォーマンスの測定方法、トレードオフについて説明します。
サービス ワーカーを検討する主な理由
ウェブアプリにサービス ワーカーを追加することは、サイトのアーキテクチャを変更することと同じように、明確な目標を設定して行う必要があります。Google 検索チームにとって、サービス ワーカーの追加を検討する価値がある主な理由はいくつかありました。
検索結果のキャッシュ保存の制限
Google 検索チームは、ユーザーが短期間に同じキーワードを複数回検索することが多いことを発見しました。検索チームは、同じ結果が得られる可能性が高いにもかかわらず、新しいバックエンド リクエストをトリガーするのではなく、キャッシュ保存を活用して、これらの繰り返しリクエストをローカルで処理したいと考えていました。
鮮度は重要であり、ユーザーは進化し続けるトピックについて最新の結果を期待して、同じキーワードで繰り返し検索することがあります。サービス ワーカーを使用することで、検索チームはローカルにキャッシュ保存された検索結果の有効期間を制御するきめ細かいロジックを実装し、ユーザーに最適な速度と鮮度のバランスを実現できます。
有意義なオフライン エクスペリエンス
また、Google 検索チームは、オフラインでも有意義な体験を提供したいと考えていました。ユーザーは、あるトピックについて調べたいときに、インターネット接続がアクティブかどうかを気にすることなく、Google 検索ページに直接アクセスして検索を開始したいと考えています。
サービス ワーカーがない場合、オフライン時に Google 検索ページにアクセスすると、ブラウザの標準のネットワーク エラーページが表示されるだけです。ユーザーは、接続が復旧したら戻って再試行することを覚えておく必要があります。サービス ワーカーを使用すると、カスタムのオフライン HTML レスポンスを提供し、ユーザーが検索クエリをすぐに実行できるようになります。

結果はインターネット接続が確立されるまで利用できませんが、サービス ワーカーを使用すると、検索を延期して、デバイスがオンラインに戻るとすぐに Background Sync API を使用して Google のサーバーに送信できます。
JavaScript のキャッシュ保存と配信のスマート化
もう 1 つの動機は、検索結果ページのさまざまな種類の機能を強化するモジュール化された JavaScript コードのキャッシュ保存と読み込みを最適化することでした。JavaScript バンドルには、サービス ワーカーが関与しない場合にメリットがあるため、検索チームはバンドルを完全に停止したくありませんでした。
検索チームは、サービス ワーカーの機能を利用して、JavaScript の細かいチャンクをバージョン管理し、実行時にキャッシュに保存することで、キャッシュのチャーン率を減らし、今後再利用される JavaScript を効率的にキャッシュに保存できるのではないかと考えていました。サービス ワーカー内のロジックは、複数の JavaScript モジュールを含むバンドルの送信 HTTP リクエストを分析し、ローカルにキャッシュ保存された複数のモジュールを組み合わせてリクエストを満たします。つまり、可能な場合は「バンドル解除」を行います。これにより、ユーザーの帯域幅が節約され、全体的な応答性が向上します。
サービス ワーカーによって提供されるキャッシュに保存された JavaScript を使用することには、パフォーマンス上のメリットもあります。Chrome では、その JavaScript の解析済みのバイトコード表現が保存され、再利用されるため、ページで JavaScript を実行するためにランタイムで実行する必要がある作業が少なくなります。
課題と解決策
チームが掲げた目標を達成するために克服する必要があったハードルをいくつかご紹介します。これらの課題の中には Google 検索に固有のものもありますが、サービス ワーカーの導入を検討している多くのサイトにも当てはまるものがあります。
問題: サービス ワーカーのオーバーヘッド
Google 検索でサービス ワーカーをリリースするうえで最大の課題であり、真の障害となっていたのは、ユーザーが認識するレイテンシが増加するような処理を行わないようにすることでした。Google 検索はパフォーマンスを非常に重視しており、過去には、特定のユーザー層に対して数十ミリ秒の遅延が生じる場合でも、新機能のリリースをブロックしたことがあります。
チームが初期のテストでパフォーマンス データの収集を開始したとき、問題が発生することは明らかでした。検索結果ページのナビゲーション リクエストに対するレスポンスとして返される HTML は動的であり、検索のウェブサーバーで実行する必要があるロジックによって大きく異なります。現在、サービス ワーカーがこのロジックを複製してキャッシュに保存された HTML をすぐに返す方法はありません。サービス ワーカーができるのは、ナビゲーション リクエストをバックエンドのウェブサーバーに渡すことだけです。これにはネットワーク リクエストが必要になります。
サービス ワーカーがない場合、このネットワーク リクエストはユーザーのナビゲーションと同時に発生します。サービス ワーカーが登録されると、フェッチ ハンドラがネットワークにアクセスする以外に何も行わない場合でも、常に起動して fetch イベント ハンドラを実行する機会が与えられます。サービス ワーカー コードの起動と実行にかかる時間は、すべてのナビゲーションに追加される純粋なオーバーヘッドです。

これにより、サービス ワーカーの実装は、他のメリットを正当化するにはレイテンシのデメリットが大きすぎます。また、実際のデバイスでサービス ワーカーの起動時間を測定した結果、起動時間の分布が広範囲に及ぶことがわかりました。一部のローエンド モバイル デバイスでは、サービス ワーカーの起動に、結果ページの HTML のネットワーク リクエストにかかる時間とほぼ同じ時間がかかっていました。
解決策: ナビゲーション プリロードを使用する
Google 検索チームがサービス ワーカーのリリースを進めることができた最も重要な機能は、ナビゲーションのプリロードです。ナビゲーション プリロードを使用することは、ネットワークからのレスポンスを使用してナビゲーション リクエストを満たす必要があるサービス ワーカーにとって、パフォーマンスを大幅に向上させるための重要な手段です。これは、Service Worker の起動と同時にナビゲーション リクエストをすぐに開始するようブラウザにヒントを提供します。

サービス ワーカーの起動にかかる時間がネットワークからレスポンスが返ってくるまでの時間よりも短ければ、サービス ワーカーによってレイテンシのオーバーヘッドが発生することはありません。
また、サービス ワーカーの起動時間がナビゲーション リクエストを超える可能性がある低価格帯のモバイル デバイスでは、サービス ワーカーの使用を避ける必要もありました。「ローエンド」デバイスの明確な定義がないため、デバイスに搭載されている RAM の合計を確認するというヒューリスティックが考案されました。メモリが 2 GB 未満のデバイスはローエンド デバイスのカテゴリに分類され、サービス ワーカーの起動時間が許容できないほど長くなります。
将来の使用のためにキャッシュに保存されるリソースのフルセットは数メガバイトに及ぶ可能性があるため、利用可能なストレージ容量も考慮する必要があります。navigator.storage インターフェースを使用すると、Google 検索ページは、ストレージ割り当ての失敗によりデータ キャッシュ保存の試みが失敗するリスクがあるかどうかを事前に把握できます。
これにより、検索チームは、サービス ワーカーを使用するかどうかを判断するために使用できる複数の条件を把握しました。ユーザーがナビゲーション プリロードをサポートするブラウザを使用して Google 検索ページにアクセスし、2 ギガバイト以上の RAM と十分な空きストレージ容量がある場合、サービス ワーカーが登録されます。この条件を満たしていないブラウザやデバイスでは、サービス ワーカーは利用できませんが、これまでと同じ Google 検索の機能は引き続きご利用いただけます。
この選択的登録のメリットの一つは、より小さく効率的なサービス ワーカーを配信できることです。サービス ワーカー コードを実行する対象を比較的新しいブラウザにすることで、古いブラウザ用のトランスパイルとポリフィルのオーバーヘッドを排除できます。これにより、サービス ワーカーの実装の合計サイズから約 8 キロバイトの非圧縮 JavaScript コードが削減されました。
問題: サービス ワーカーのスコープ
検索チームがレイテンシに関する十分な実験を行い、ナビゲーション プリロードを使用することで、サービス ワーカーを使用するためのレイテンシに影響しない実行可能なパスが提供されると確信した時点で、いくつかの実用的な問題が表面化し始めました。その問題の 1 つは、サービス ワーカーのスコープ ルールに関連しています。Service Worker のスコープは、制御できる可能性のあるページを決定します。
スコープは URL パス プレフィックスに基づいて機能します。単一のウェブアプリをホストするドメインの場合、通常は最大スコープの / を持つ Service Worker を使用するだけなので、問題はありません。この Service Worker はドメイン内の任意のページを制御できます。しかし、Google 検索の URL 構造はもう少し複雑です。
サービス ワーカーに / の最大スコープが与えられた場合、www.google.com(またはリージョン相当)でホストされているすべてのページを制御できるようになりますが、そのドメインには Google 検索とは関係のない URL があります。より合理的で制限的なスコープは /search です。これにより、検索結果とまったく関係のない URL を少なくとも排除できます。
残念ながら、この /search URL パスは、さまざまな種類の Google 検索結果で共有されており、URL クエリ パラメータによって、どの種類の検索結果が表示されるかが決まります。これらのフレーバーの一部では、従来のウェブ検索結果ページとはまったく異なるコードベースが使用されています。たとえば、画像検索とショッピング検索はどちらも /search URL パスで異なるクエリ パラメータを使用して提供されていますが、どちらのインターフェースも独自のサービス ワーカー エクスペリエンスをリリースする準備ができていませんでした。
解決策: ディスパッチとルーティングのフレームワークを作成する
URL パス接頭辞よりも強力な方法でサービス ワーカーのスコープを決定できるようにする提案もありますが、Google 検索チームは、制御するページの一部に対して何も行わないサービス ワーカーをデプロイすることに苦労していました。
この問題を回避するため、Google 検索チームは、クライアント ページのクエリ パラメータなどの条件をチェックするように構成し、それらの条件を使用してどの特定のコードパスをたどるかを決定できる、カスタムのディスパッチとルーティングのフレームワークを構築しました。ルールをハードコードするのではなく、システムは柔軟に構築されており、画像検索やショッピング検索など、URL スペースを共有するチームが、後でサービス ワーカーのロジックを実装することにした場合に、そのロジックを組み込めるようになっています。
問題: パーソナライズされた結果と指標
ユーザーは Google アカウントを使用して Google 検索にログインできます。検索結果は、ユーザーのアカウント データに基づいてカスタマイズされる場合があります。ログイン ユーザーは、特定のブラウザ Cookie によって識別されます。これは、広くサポートされている確立された標準です。
ただし、ブラウザ Cookie を使用するデメリットとして、Service Worker 内で公開されないため、値を自動的に調べて、ユーザーのログアウトやアカウントの切り替えによって値が変更されていないことを確認する方法がありません。(サービス ワーカーで Cookie にアクセスできるようにする取り組みが進められていますが、この記事の執筆時点では、このアプローチは試験運用であり、広くサポートされていません)。
サービス ワーカーが認識している現在のログイン ユーザーと、Google 検索のウェブ インターフェースに実際にログインしているユーザーが一致しない場合、検索結果が誤ってパーソナライズされたり、指標とロギングが誤って帰属されたりする可能性があります。これらの障害シナリオのいずれかが発生すると、Google 検索チームにとって重大な問題となります。
解決策: postMessage を使用して Cookie を送信する
Google 検索チームは、試験運用版の API がリリースされ、サービス ワーカー内のブラウザの Cookie に直接アクセスできるようになるのを待つのではなく、一時的な解決策を採用しました。サービス ワーカーによって制御されるページが読み込まれるたびに、ページは関連する Cookie を読み取り、postMessage() を使用してサービス ワーカーに送信します。
サービス ワーカーは、現在の Cookie の値を想定される値と照合し、一致しない場合は、ストレージからユーザー固有のデータを削除し、誤ったパーソナライズなしで検索結果ページを再読み込みします。
サービス ワーカーがベースラインにリセットする具体的な手順は Google 検索の要件に固有のものですが、ブラウザの Cookie に基づいてパーソナライズされたデータを扱う他のデベロッパーにも、同じ一般的なアプローチが役立つ可能性があります。
問題: テストと動的
前述のとおり、Google 検索チームは本番環境でのテストを重視しており、新しいコードや機能の効果を実際にテストしてからデフォルトで有効にしています。キャッシュされたデータに大きく依存する静的サービス ワーカーでは、ユーザーのテストへの参加とテストからの離脱にバックエンド サーバーとの通信が必要になることが多いため、この点が課題になることがあります。
解決策: 動的に生成されたサービス ワーカー スクリプト
チームが採用した解決策は、事前に生成される単一の静的サービス ワーカー スクリプトではなく、個々のユーザーごとにウェブサーバーによってカスタマイズされた動的に生成されるサービス ワーカー スクリプトを使用することでした。サービス ワーカーの動作やネットワーク リクエスト全般に影響する可能性のあるテストに関する情報は、このカスタマイズされたサービス ワーカー スクリプトに直接含まれています。ユーザーのアクティブなエクスペリエンスのセットを変更するには、ブラウザ Cookie などの従来の手法と、登録されたサービス ワーカー URL で更新されたコードを提供する手法を組み合わせます。
動的に生成されたサービス ワーカー スクリプトを使用すると、サービス ワーカーの実装に回避する必要がある致命的なバグが発生した場合に、エスケープ ハッチを簡単に提供できます。動的サーバー ワーカーのレスポンスは no-op 実装になる可能性があり、その場合、一部またはすべての現在のユーザーに対してサービス ワーカーが事実上無効になります。
問題: アップデートの調整
実際のサービス ワーカーのデプロイで最も難しい課題の 1 つは、ネットワークを回避してキャッシュを優先する一方で、既存のユーザーが重要な更新や変更を本番環境へのデプロイ後すぐに受け取れるように、妥当なトレードオフを考案することです。適切なバランスは、さまざまな要因によって異なります。
- ウェブアプリが、ユーザーが新しいページに移動することなく、無期限に開いたままにするシングル ページ アプリであるかどうか。
- バックエンド ウェブサーバーの更新のデプロイ頻度。
- 平均的なユーザーがウェブアプリの少し古いバージョンを使用することを許容するかどうか、または鮮度が最優先事項かどうか。
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 の追加部分を挿入することを意味します。これらのレスポンスがネットワークではなくローカル キャッシュから返される場合、サービス ワーカーを実行するオーバーヘッドは、キャッシュ ファーストによるパフォーマンスの向上と比較して通常は無視できる程度です。ただし、ナビゲーション リクエストを処理する際にサービス ワーカーが常にネットワークを参照する必要があることがわかっている場合は、ナビゲーション プリロードを使用することでパフォーマンスを大幅に改善できます。
Service Worker は(依然として)プログレッシブ エンハンスメントです
サービス ワーカーのサポート状況は、1 年前と比べてもはるかに明るいものになっています。現在のすべてのブラウザで、少なくともService Worker のサポートが提供されていますが、残念ながら、バックグラウンド同期やナビゲーションのプリロードなど、高度な Service Worker の機能は、すべてのブラウザで利用できるわけではありません。必要な機能の特定のサブセットの機能チェックを行い、それらが存在する場合にのみサービス ワーカーを登録するというアプローチは、依然として妥当です。
同様に、実際の環境でテストを実施し、サービス ワーカーの追加オーバーヘッドによってローエンド デバイスのパフォーマンスが低下することがわかっている場合は、そのようなシナリオでサービス ワーカーを登録しないようにすることもできます。
サービス ワーカーは、すべての前提条件が満たされ、サービス ワーカーがユーザー エクスペリエンスと全体的な読み込みパフォーマンスにプラスの効果をもたらす場合にウェブアプリに追加されるプログレッシブ エンハンスメントとして扱う必要があります。
すべてを数値で測る
サービス ワーカーの導入がユーザー エクスペリエンスにプラスの影響を与えたのか、マイナスの影響を与えたのかを判断する唯一の方法は、テストを実施して結果を測定することです。
意味のある測定を設定する具体的な方法は、使用している分析プロバイダと、デプロイ設定で通常どのようにテストを実施しているかによって異なります。Google アナリティクスを使用して指標を収集するアプローチについては、Google I/O ウェブアプリでサービス ワーカーを使用した経験に基づくこちらのケーススタディで詳しく説明しています。
目標とすべきでないこと
ウェブ開発コミュニティの多くは Service Worker をプログレッシブ ウェブアプリと関連付けていますが、「Google 検索 PWA」の構築はチームの当初の目標ではありませんでした。Google 検索ウェブアプリは、ウェブアプリ マニフェストでメタデータを提供しておらず、ユーザーにホーム画面に追加するフローを促すこともありません。検索チームは、Google 検索の従来のエントリ ポイントからユーザーがウェブアプリにアクセスしていることに満足しています。
Google 検索のウェブ エクスペリエンスをインストール済みアプリと同等のものにしようとするのではなく、既存のウェブサイトを段階的に強化することに重点を置いて、最初のリリースが行われました。
謝辞
サービス ワーカーの実装にご尽力いただいた Google 検索のウェブ開発チームの皆様、また、この記事の執筆に役立つ背景資料をご提供いただいた皆様に感謝いたします。特に、Philippe Golle、Rajesh Jagannathan、R. Samuel Klatchko、Andy Martone、Leonardo Peña、Rachel Shearer、Greg Terrono、Clay Woolam。
更新(2021 年 10 月): この記事の公開後、Google 検索チームは現在のサービス ワーカー アーキテクチャのメリットとトレードオフを再評価しました。上記のサービス ワーカーは廃止されます。Google 検索のウェブ インフラストラクチャが進化するにつれて、チームは Service Worker の設計を再検討する可能性があります。