Application Shell アーキテクチャによるウェブアプリの即時読み込み

Addy Osmani 氏
Addy Osmani
Matt Gaunt 氏

アプリケーション シェルは、ユーザー インターフェースの基盤となる最小限の HTML、CSS、JavaScript です。アプリケーション シェルでは次のことを行う必要があります。

  • 読み込みが速い
  • キャッシュに保存される
  • コンテンツを動的に表示する

アプリケーション シェルは、高いパフォーマンスを確実に実現する秘訣です。アプリのシェルは、ネイティブ アプリを作成する場合、アプリストアに公開するコードのバンドルのようなものと考えてください。使用を開始するために必要な負荷ですが、すべてではありません。UI をローカルに保ち、API を介してコンテンツを動的に取得します。

App Shell: HTML、JS、CSS シェルと HTML コンテンツの分離

背景

Alex Russell の「プログレッシブ ウェブアプリ」という記事では、オフライン サポート、プッシュ通知、ホーム画面への追加機能を備えたネイティブ アプリのような操作性を実現するために、使用とユーザーの同意によってウェブアプリが段階的に変化することが説明されています。これは、Service Worker の機能とパフォーマンス上のメリットとキャッシュ能力に大きく依存します。そのため、ウェブ アプリケーションでも、ネイティブ アプリと同じ即時読み込みと定期的なアップデートが提供されるため、スピードを重視できます。

こうした機能を最大限に活用するには、ウェブサイトに対する新しい考え方、アプリケーション シェル アーキテクチャが必要です。

ここでは、Service Worker で拡張されたアプリケーション シェル アーキテクチャを使用してアプリを構築する方法を見ていきましょう。クライアントサイドとサーバーサイドのレンダリングについて解説し、今すぐ試せるエンドツーエンドのサンプルを紹介します。

次の例では、このアーキテクチャを使用したアプリの最初の読み込みを示しています。画面下部に「アプリのオフライン利用の準備ができました」というトーストが表示されます。シェルのアップデートが後で利用可能になった場合は、新しいバージョンに更新するようユーザーに通知できます。

アプリケーション シェルの DevTools で実行されている Service Worker の画像

Service Worker とは何でしょうか。

Service Worker は、ウェブページとは別にバックグラウンドで実行されるスクリプトです。提供するページからのネットワーク リクエストやサーバーからの通知のプッシュなどのイベントに応答します。Service Worker の存続期間は意図的に短くなっています。イベントを受信すると起動し、その処理に必要な期間だけ実行されます。

また、通常のブラウジング環境における JavaScript と比べると、Service Worker の API セットは限られています。これはウェブ版のワーカー向けの標準的な機能です。Service Worker は DOM にアクセスできませんが、Cache API などにはアクセスでき、Fetch API を使用してネットワーク リクエストを行うことができます。Service Worker とそれを制御するページとの間で、データの永続性とメッセージングのために IndexedDB APIpostMessage() を使用することもできます。サーバーから送信されるプッシュ イベントは、Notification API を呼び出して、ユーザー エンゲージメントを高めることができます。

Service Worker は、ページから行われたネットワーク リクエストをインターセプトし(Service Worker のフェッチ イベントをトリガーし)、ネットワークから取得したり、ローカル キャッシュから取得したり、さらにはプログラムで作成されたレスポンスを返したりできます。実質的に、これはブラウザでプログラム可能なプロキシです。重要なのは、レスポンスの送信元にかかわらず、Service Worker が関与していないかのようにウェブページを見ることです。

Service Worker の詳細については、Service Worker の概要をご覧ください。

パフォーマンス上のメリット

Service Worker はオフライン キャッシュ保存に優れていますが、サイトやウェブアプリに繰り返しアクセスする場合の即時読み込みという形でパフォーマンスを大きく向上させます。アプリケーション シェルをキャッシュに保存すると、オフラインで動作し、JavaScript を使用してコンテンツを入力できます。

その結果、ネットワークが接続されていない状態で画面上に有意なピクセルを表示できます。コンテンツが最終的にそのネットワークから取得された場合でも、ツールバーとカードをすぐに表示し、残りのコンテンツを段階的に読み込むと考えることができます。

このアーキテクチャを実際のデバイスでテストするために、WebPageTest.orgアプリケーション シェルのサンプルを実行し、以下の結果を表示しました。

テスト 1: Chrome Dev を使用して Nexus 5 でケーブルをテストする

アプリの最初のビューでは、ネットワークからすべてのリソースを取得する必要があり、1.2 秒が経過するまで意味のある描画は行われません。Service Worker のキャッシュ保存のおかげで、再訪問は意味のある描画を達成し、読み込みは 0.5 秒で完全に完了します。

ケーブル接続のウェブページのテスト ペイント図

テスト 2: Chrome Dev を使用して Nexus 5 で 3G でテストする

少し遅めの 3G 接続を使ってサンプルをテストすることもできます。今回は、First Meaningful Paint の初回アクセスに 2.5 秒かかります。ページが完全に読み込まれるまでに 7.1 秒かかります。Service Worker のキャッシュ保存により、再訪問によって意味のある描画が実現され、読み込みが完全に 0.8 秒で完了します。

3G 接続のウェブページ テスト ペイント ダイアグラム

他のビューでも同様のストーリーを示しています。アプリケーション シェルで First Meaningful Paint に到達するまでに要する 3 秒を比較します。

Web Page Test の最初のビューのタイムラインを描画する

同じページが Service Worker キャッシュから読み込まれると 0.9 秒になります。エンドユーザーのために 2 秒以上の時間を節約。

ウェブページ テストの繰り返しビューのタイムラインを描画する

アプリケーション シェル アーキテクチャを使用すると、独自のアプリケーションでも、同様の信頼性の高いパフォーマンス向上を実現できます。

Service Worker を導入するには、アプリ構造の見直しが必要ですか?

Service Worker を使用すると、アプリケーション アーキテクチャに若干の変更が生じます。アプリケーションのすべてを HTML 文字列にまとめるのではなく、AJAX スタイルで処理することをおすすめします。ここには、シェル(常にキャッシュされ、ネットワークがなくても常に起動できる)と、定期的に更新され、個別に管理されるコンテンツがあります。

この分割の影響は大きく、初回アクセス時に、サーバー上でコンテンツをレンダリングし、クライアントに Service Worker をインストールできます。その後の訪問では、データのリクエストのみが必要になります。

プログレッシブ エンハンスメントについては、

現在、Service Worker はすべてのブラウザでサポートされているわけではありませんが、アプリケーション コンテンツのシェル アーキテクチャではプログレッシブ エンハンスメントを使用して、誰もがコンテンツにアクセスできるようにしています。たとえば、サンプル プロジェクトを見てみましょう。

以下は、Chrome、Firefox Nightly、Safari でレンダリングされたフルバージョンです。左端は Safari バージョンで、Service Worker を使用せずにサーバーにコンテンツがレンダリングされています。右側には、Service Worker を利用した Chrome と Firefox Nightly のバージョンが示されています。

Safari、Chrome、Firefox で読み込まれたアプリケーション シェルの画像

このアーキテクチャを使用するのはどのような場合ですか。

Application Shell アーキテクチャは、動的なアプリやサイトに最適です。サイトが小規模で静的である場合は、おそらくアプリケーション シェルは必要なく、Service Worker の oninstall ステップでサイト全体をキャッシュするだけで済みます。プロジェクトに最も適したアプローチを使用してください。すでに多くの JavaScript フレームワークでは、アプリケーション ロジックをコンテンツから分離することが推奨されており、このパターンをより簡単に適用できます。

このパターンを使用している製品版アプリはありますか?

このアプリケーション シェル アーキテクチャは、アプリケーション全体の UI に少し変更を加えるだけで利用でき、Google の I/O 2015 プログレッシブ ウェブアプリや Google の受信トレイなどの大規模なサイトで適切に機能しています。

Google 受信トレイを読み込んでいます。Service Worker を使った受信トレイの図。

オフライン アプリケーション シェルはパフォーマンスを大きく向上させるものであり、Jake Archibald のオフラインの Wikipedia アプリFlipkart Lite のプログレッシブ ウェブアプリでも実証されています。

Jake Archibald 氏の Wikipedia デモのスクリーンショット。

アーキテクチャの説明

初回読み込みの際の目標は、意味のあるコンテンツをできるだけ早くユーザーの画面に表示することです。

最初に他のページを読み込む

App Shell による最初の読み込みの図

一般に、Application shell アーキテクチャは、以下のことを行います。

  • 初期読み込みを優先します。ただし、Service Worker がアプリケーション シェルをキャッシュに保存するようにして、次回アクセスしてもネットワークからシェルを再取得する必要がなくなります。

  • それ以外はすべて遅延読み込みまたはバックグラウンドで読み込みます。動的コンテンツには読み取りキャッシュを使用することをおすすめします。

  • 静的コンテンツを管理する Service Worker を確実にキャッシュして更新するには、sw-precache などの Service Worker ツールを使用します。(sw-precache については後で詳しく説明します)。

手順は次のとおりです。

  • サーバーは、クライアントがレンダリングできる HTML コンテンツを送信し、Service Worker をサポートしていないブラウザに対応するために、遠く離れた HTTP キャッシュ有効期限ヘッダーを使用します。ハッシュを使用してファイル名を提供し、「バージョニング」と、アプリケーションのライフサイクルの後半で容易な更新の両方を可能にします。

  • ページでは、ドキュメント <head> 内の <style> タグにインライン CSS スタイルが追加され、アプリケーション シェルの初回ペイントがすばやく行われます。各ページは、現在のビューに必要な JavaScript を非同期で読み込みます。CSS を非同期で読み込むことはできないため、パーサー ドリブンや同期ではなく非同期である JavaScript を使用して、スタイルをリクエストできます。また、requestAnimationFrame() を利用すると、キャッシュ ヒットが早く起きて、スタイルが誤ってクリティカル レンダリング パスの一部になってしまう状況を回避できます。requestAnimationFrame() は、スタイルを読み込む前に最初のフレームを強制的にペイントします。また、Filament Group の loadCSS などのプロジェクトを使用して、JavaScript で非同期に CSS をリクエストする方法もあります。

  • Service Worker は、アプリケーション シェルのキャッシュ エントリを保存します。これにより、ネットワーク上で利用可能な更新がない限り、繰り返しアクセスすると Service Worker のキャッシュからシェル全体を読み込めるようになります。

コンテンツ向け App Shell

実践的な実装

アプリケーション シェル アーキテクチャ、クライアント用の標準 JavaScript ES2015、サーバー用の Express.js を使用して、完全に機能するサンプルを作成しました。もちろん、クライアント部分とサーバー部分のどちらにも独自のスタック(PHP、Ruby、Python など)を使用することを止めるものはありません。

Service Worker のライフサイクル

Application Shell プロジェクトでは、sw-precache を使用します。これは、次のような Service Worker のライフサイクルを提供します。

イベント アクション
インストール アプリケーション シェルとその他のシングルページ アプリリソースをキャッシュに保存する。
有効化 古いキャッシュを削除します。
フェッチ URL 用に単一ページのウェブアプリを提供し、アセットと事前定義済みの部分にキャッシュを使用します。他のリクエストにはネットワークを使用します。

サーバービット

このアーキテクチャでは、サーバー側コンポーネント(この場合は Express で記述)がコンテンツとプレゼンテーションを別々に扱う必要があります。HTML レイアウトにコンテンツを追加すると、ページが静的にレンダリングされる場合もあれば、個別に配信して動的に読み込まれる場合もあります。

当然のことながら、サーバーサイドの設定は、デモアプリで使用するものと大きく異なる場合があります。このウェブアプリのパターンは、ほとんどのサーバー設定で実現できますが、一部再設計が必要です。次のモデルが非常にうまく機能することがわかりました。

App Shell アーキテクチャの図
  • エンドポイントは、ユーザー向けの URL(インデックス/ワイルドカード)、アプリケーション シェル(Service Worker)、HTML の部分という、アプリケーションの 3 つの部分に対して定義されます。

  • 各エンドポイントには、handlebars レイアウトを pull するコントローラがあり、次に、ハンドルバーのパーシャルとビューを pull できます。簡単に言うと、パーシャルは、最終ページにコピーされる HTML のまとまったビューです。 注: 多くの場合、高度なデータ同期を行う JavaScript フレームワークの方が、Application Shell アーキテクチャに簡単に移植できます。部分的なものではなく、データ バインディングと同期を使用する傾向がある。

  • ユーザーには、最初にコンテンツを含む静的ページが配信されます。このページで Service Worker が登録されます(サポートされている場合)。Service Worker は、Application Shell とそれに依存するすべての要素(CSS、JS など)をキャッシュに保存します。

  • これにより、App Shell は単一ページのウェブアプリとして機能し、特定の URL のコンテンツで JavaScript を使用して XHR を実行します。XHR の呼び出しは /partials* エンドポイントに対して行われます。このエンドポイントは、コンテンツの表示に必要な HTML、CSS、JS の小さなチャンクを返します。注: このアプローチには多くの方法がありますが、XHR はそのうちの 1 つにすぎません。アプリケーションによっては、最初のレンダリングでデータをインライン化するため(JSON を使用している場合もあるため)、フラット化された HTML という意味で「静的」ではありません。

  • Service Worker がサポートされていないブラウザには、常にフォールバックが提供されます。このデモでは、基本的な静的なサーバーサイド レンダリングにフォールバックしますが、これは多くのオプションの 1 つにすぎません。Service Worker には、キャッシュされたアプリケーション シェルを使用して単一ページ アプリケーション スタイルのアプリのパフォーマンスを改善する新たな機能があります。

ファイルのバージョニング

ここで生じる問題の一つは、ファイルのバージョニングと更新をどのように処理するかです。これはアプリケーション固有で、次のオプションがあります。

  • 最初にネットワークを使用し、それ以外の場合はキャッシュ バージョンを使用します。

  • オフラインの場合に障害が発生します。

  • 古いバージョンをキャッシュに保存して、後で更新する。

Application Shell 自体については、Service Worker の設定でキャッシュ ファーストのアプローチを採用する必要があります。アプリケーション シェルをキャッシュしていないのであれば、このアーキテクチャは適切に導入されていません。

ツール

Google は、アプリケーションのシェルの事前キャッシュや一般的なキャッシュ パターンの処理のプロセスを簡単にセットアップできるように、さまざまな Service Worker ヘルパー ライブラリを保守しています。

ウェブの基礎における Service Worker Library のサイトのスクリーンショット

アプリケーション シェルで sw-precache を使用する

sw-precache を使用して Application shell をキャッシュすることで、ファイルのリビジョン、インストールと有効化に関する質問、App Shell の取得シナリオに関する懸念を解消できます。sw-precache をアプリケーションのビルドプロセスにドロップし、設定可能なワイルドカードを使用して静的リソースを取得する。Service Worker スクリプトを手動で作成するのではなく、sw-precache でキャッシュファースト フェッチ ハンドラを使用して、安全で効率的なキャッシュを管理するスクリプトを生成しましょう。

アプリへの初回アクセス時に、必要なリソース一式が事前キャッシュされます。これは、アプリストアからネイティブ アプリをインストールする場合とほぼ同じです。ユーザーがアプリに戻ると、更新されたリソースのみがダウンロードされます。デモでは、新しいシェルが利用可能になると「アプリのアップデート。新しいバージョンに更新してください。」このパターンにより、最新バージョンに更新できることをユーザーに簡単に知らせることができます。

ランタイム キャッシュに sw-toolbox を使用する

sw-toolbox を使用してランタイム キャッシュを実行します。方法はリソースに応じて異なります。

  • イメージに対する cacheFirst。カスタムの有効期限ポリシーが N maxEntries に設定された専用の名前付きキャッシュも用意されています。

  • networkFirst か最速の API リクエストを使用します。最速でも問題ないかもしれませんが、頻繁に更新される特定の API フィードがある場合は、networkFirst を使用します。

まとめ

Application Shell アーキテクチャにはいくつかのメリットがありますが、一部のクラスのアプリケーションにのみ有効です。このモデルはまだ新しいので、このアーキテクチャの労力と全体的なパフォーマンス上のメリットを評価する価値があります。

Google のテストでは、クライアントとサーバー間のテンプレート共有を利用して、2 つのアプリケーション レイヤを構築する作業を最小限に抑えました。これにより、段階的な補正が引き続き重要な機能となります。

すでにアプリで Service Worker の使用を検討している場合は、そのアーキテクチャを確認して、ご自身のプロジェクトに適しているかどうかを評価してください。

Jeff Posnick、Paul Lewis、Alex Russell、Seth Thompson、Rob Dodson、Taylor Savage、Joe Medley のレビュアーに感謝します。