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

アプリケーション シェルは、ユーザー インターフェースを強化する最小限の HTML、CSS、JavaScript です。アプリケーション シェルは次の動作を実行する必要があります。

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

Application Shell は優れたパフォーマンスを確実に実現するための秘訣です。アプリのシェルは、ネイティブ アプリを構築する際にアプリストアに公開するコードセットのようなものだと考えてください。軌道に乗るために必要となる負荷ですが、全体像とは言えないかもしれません。UI をローカルに保持し、API を介してコンテンツを動的に pull します。

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

背景情報

Alex Russell のプログレッシブ ウェブアプリの記事では、オフライン サポート、プッシュ通知、ホーム画面への追加機能など、ネイティブ アプリに近いエクスペリエンスを提供するために、ウェブアプリの使用とユーザーの同意を段階的に変化させる方法を説明しています。これは、Service Worker の機能とパフォーマンス上のメリットと、そのキャッシュ機能に大きく依存します。ネイティブ アプリと同じように、ウェブアプリを瞬時に読み込みでき、定期的な更新も行えるため、スピードを重視できます。

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

ここでは、Service Worker で拡張されたアプリケーション シェル アーキテクチャを使用してアプリを構造化する方法を説明します。ここでは、クライアント側とサーバー側の両方のレンダリングについて説明し、すぐに試すことができるエンドツーエンドのサンプルを共有します。

ポイントを明確にするために、以下の例は、このアーキテクチャを使用したアプリの最初の読み込みを示しています。画面下部に「App is ready for offline use」というトーストが表示されます。その後、シェルのアップデートが利用可能になったら、新しいバージョンに更新するようにユーザーに通知できます。

DevTools でアプリケーション シェル用に動作している Service Worker の画像

Service Worker とは

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

また、通常のブラウジング環境での JavaScript と比較すると、Service Worker で使用できる API は限られています。これはウェブ上のワーカーの標準です。Service Worker は DOM にはアクセスできませんが、Cache API などにはアクセスできます。また、Fetch API を使用してネットワーク リクエストを送信することもできます。IndexedDB APIpostMessage() を使用して、Service Worker と Service Worker が制御するページ間のデータの永続性とメッセージングに使用することもできます。サーバーから送信されたプッシュ イベント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 接続でサンプルをテストすることもできます。初回訪問で初めて意味のあるペイントを行うまで 2.5 秒かかりました。ページが完全に読み込まれるまでに 7.1 秒かかります。Service Worker のキャッシュ保存により、再訪問により意味のあるペイントが行われ、0.8 秒で読み込みが完了します。

3G 接続の Web ページのテストペイント図

他のビューでも同様の状況です。アプリケーション シェルで最初の有意義な描画が完了するまでに 3 秒かかる場合を比較してください。

Web Page Test で最初の表示のタイムラインをペイント

同じページが Service Worker のキャッシュから読み込まれると 0.9 秒かかるようになります。エンドユーザーの作業時間が 2 秒に短縮されます。

Web Page Test の繰り返し表示のタイムラインをペイント

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

Service Worker はアプリの構成を見直す必要に迫られていますか?

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

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

プログレッシブ エンハンスメントについてはどうでしょうか。

Service Worker は現在すべてのブラウザでサポートされているわけではありませんが、アプリケーション コンテンツ シェル アーキテクチャでは、すべてのユーザーがコンテンツにアクセスできるように段階的な機能強化が行われています。例として、サンプル プロジェクトを見てみましょう。

以下は、Chrome、Firefox Nightly、Safari でレンダリングされた完全版です。左端の Safari バージョンでは、Service Worker を使用しないでコンテンツがサーバー上でレンダリングされています。右側は、Service Worker を搭載した Chrome と Firefox のナイトリー版です。

Safari、Chrome、Firefox に読み込まれた Application Shell の画像

このアーキテクチャの使用が適している状況

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

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

このアプリケーション シェル アーキテクチャは、アプリケーションの UI を少し変更するだけで使用可能です。これまでは、Google の I/O 2015 Progressive Web App や Google の受信トレイといった大規模なサイトでうまく機能してきました。

Google 受信トレイを読み込んでいる画像。Service Worker を使用した受信トレイを示しています。

オフラインのアプリケーション シェルはパフォーマンスに大きな影響を与え、Jake Archibald の Wikipedia オフライン アプリFlipkart Lite のプログレッシブ ウェブアプリでも十分に実証されています。

Jake Archibald によるウィキペディアのデモのスクリーンショット。

アーキテクチャの説明

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

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

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

一般的に、このアプリケーション シェル アーキテクチャでは以下の処理が行われます。

  • 初期読み込みを優先的に行いますが、Service Worker でアプリケーション シェルのキャッシュ保存を行うようにして、再訪問時にネットワークからシェルを再取得する必要がないようにします。

  • その他すべての遅延読み込みまたはバックグラウンド読み込み。動的コンテンツにはリードスルー キャッシュを使用するのも 1 つの方法です。

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

これを実現するには:

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

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

  • Service Worker は、Application Shell のキャッシュ エントリを保存します。これにより、ネットワーク上でアップデートが利用可能でない限り、次回アクセス時に Service Worker のキャッシュからシェル全体を読み込むことができます。

コンテンツ用 App Shell

実践的な実装

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

Service Worker のライフサイクル

アプリケーション シェル プロジェクトでは sw-precache を使用します。これは、次のような Service Worker ライフサイクルを提供します。

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

サーバービット

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

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

App Shell アーキテクチャの図
  • エンドポイントはアプリケーションの 3 つの部分、つまりユーザーに表示される URL(インデックスまたはワイルドカード)、アプリケーション シェル(Service Worker)、HTML の部分的です。

  • 各エンドポイントには、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 はその一つにすぎません。アプリケーションの中には、最初のレンダリングでデータをインライン化する(JSON を使用するなど)ため、フラット化された HTML という意味では「静的」ではないアプリケーションもあります。

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

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

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

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

  • ネットワークのみ。オフラインの場合は失敗します。

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

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

ツール

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

ウェブの基礎の Service Worker ライブラリ サイトのスクリーンショット

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

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

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

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

ランタイム キャッシュには sw-toolbox を使用します。キャッシュ保存の方法はリソースによって異なります。

  • イメージの場合は cacheFirst のほか、カスタムの有効期限ポリシーが N maxEntries の専用の名前付きキャッシュとともに使用されます。

  • API リクエストの場合は networkFirst または最速(コンテンツの更新頻度に応じて異なります)。最速でも問題ないかもしれませんが、頻繁に更新される特定の API フィードがある場合は、networkFirst を使用します。

おわりに

アプリケーション シェル アーキテクチャにはいくつかのメリットがありますが、一部のクラスのアプリケーションにしか意味がありません。モデルはまだ新しいため、このアーキテクチャの労力と全体的なパフォーマンス上のメリットを評価する価値は十分にあります。

テストでは、2 つのアプリケーション レイヤを構築する作業を最小限に抑えるために、クライアントとサーバー間のテンプレート共有を活用しました。これにより、段階的な機能強化が引き続きコア機能であることが保証されます。

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

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