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

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

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

アプリケーション シェルは、信頼性の高い優れたパフォーマンスを実現するための秘訣です。アプリのシェルは、ネイティブ アプリをビルドする場合にアプリストアに公開するコードのバンドルに似ています。アプリの起動に必要な読み込みですが、すべてではありません。UI はローカルに保持し、API を介してコンテンツを動的に取得します。

アプリシェルの HTML、JS、CSS シェルと HTML コンテンツの分離

背景

Alex Russell のプログレッシブ ウェブアプリに関する記事では、ウェブアプリが使用とユーザーの同意を通じて段階的に変化し、オフライン サポート、プッシュ通知、ホーム画面への追加など、ネイティブ アプリに近いエクスペリエンスを提供できるようになる方法について説明しています。これは、サービス ワーカーの機能とパフォーマンスのメリット、およびキャッシュ機能に大きく依存します。これにより、速度に重点を置いて、ネイティブ アプリケーションで慣れ親しんだ即時読み込みと定期的な更新をウェブアプリにも提供できます。

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

Service Worker 拡張アプリケーション シェル アーキテクチャを使用してアプリを構造化する方法について説明します。クライアントサイド レンダリングとサーバーサイド レンダリングの両方について説明します。また、すぐに試せるエンドツーエンド サンプルもご紹介します。

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

アプリケーション シェルのデベロッパー ツールで実行されているサービス ワーカーの画像

サービス ワーカーとは

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

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

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

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

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

サービス ワーカーはオフライン キャッシュに適していますが、サイトやウェブアプリへの再訪問時にコンテンツを即座に読み込むことで、パフォーマンスを大幅に向上させることもできます。アプリシェルをキャッシュに保存してオフラインで動作させ、JavaScript を使用してコンテンツを入力できます。

そのため、コンテンツが最終的にネットワークから提供される場合でも、ユーザーが繰り返し訪問したときに、ネットワークを使用せずに有意なピクセルを画面に表示できます。ツールバーとカードをすぐに表示し、残りのコンテンツを段階的に読み込むと考えてください。

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

テスト 1: Chrome Dev を使用した Nexus 5 でのケーブル接続でのテスト

アプリの最初のビューでは、すべてのリソースをネットワークから取得する必要があります。1.2 秒経過するまで、意味のあるペイントは行われません。サーバー ワーカーのキャッシュ保存により、2 回目のアクセスでは意味のあるペイントが実現し、0.5 秒で読み込みが完了します。

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

テスト 2: Chrome Dev を使用した Nexus 5 での 3G でのテスト

3G 接続でサンプルをテストすることもできます。最初のアクセスで最初の有意なペイントが表示されるまでに 2.5 秒かかります。ページ全体が読み込まれるまでに 7.1 秒かかります。サービス ワーカーのキャッシュを使用すると、再訪問時に意味のあるペイントが実現し、0.8 秒で読み込みが完全に完了します。

3G 接続のウェブページ テストペイント図

他のビューでも同様の傾向が見られます。アプリケーション シェルで最初の有意なペイントを達成するまでにかかる 3 秒と比較してください。

ウェブページ テストの最初のビューのペイント タイムライン

同じページをサービス ワーカー キャッシュから読み込むのにかかる時間は 0.9 秒です。エンドユーザーの時間を 2 秒以上節約できます。

ウェブページ テストの繰り返し視聴のペイント タイムライン

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

サービス ワーカーを使用すると、アプリの構造を再考する必要がありますか?

Service Worker は、アプリケーション アーキテクチャに微妙な変更を加えます。アプリケーション全体を HTML 文字列に圧縮するのではなく、AJAX スタイルで処理すると便利な場合があります。シェル(常にキャッシュに保存され、ネットワークがなくても常に起動可能)と、定期的に更新され、個別に管理されるコンテンツが存在します。

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

プログレッシブ エンハンスメントはどうですか?

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

以下は、Chrome、Firefox Nightly、Safari でレンダリングされた完全版です。左端は、サービス ワーカーなしでサーバーでコンテンツがレンダリングされる Safari バージョンです。右側には、サービス ワーカーを搭載した Chrome と Firefox のナイトリー バージョンが表示されています。

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

このアーキテクチャを使用する場面

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

このパターンを使用している本番環境のアプリはありますか?

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

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

オフライン アプリケーション シェルはパフォーマンスの大幅な向上につながります。Jake Archibald の オフライン Wikipedia アプリFlipkart Lite のプログレッシブ ウェブアプリでもその効果が実証されています。

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

アーキテクチャの説明

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

初回読み込みと他のページの読み込み

アプリシェルを使用した初回読み込みの図

一般に、アプリケーション シェル アーキテクチャは次のようになります。

  • 最初の読み込みを優先しますが、サービス ワーカーがアプリケーション シェルをキャッシュに保存できるようにします。これにより、再訪問時にネットワークからシェルを再取得する必要がなくなります。

  • 残りのすべてを遅延読み込みまたはバックグラウンド読み込みします。動的コンテンツにはリードスルー キャッシュを使用することをおすすめします。

  • sw-precache などのサービス ワーカー ツールを使用して、静的コンテンツを管理するサービス ワーカーを信頼性の高い方法でキャッシュに保存し、更新します。(sw-precache については後述します)。

手順は次のとおりです。

  • サーバーは、クライアントがレンダリングできる HTML コンテンツを送信し、サービス ワーカーをサポートしていないブラウザを考慮して、遠い将来の HTTP キャッシュ有効期限ヘッダーを使用します。ハッシュを使用してファイル名を提供することによって、「バージョニング」と、アプリのライフサイクルの後半での簡単な更新の両方を可能にします。

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

  • サービス ワーカーは、アプリケーション シェルのキャッシュ エントリを保存します。これにより、ネットワークで更新が利用可能でない限り、再訪問時にシェルをサービス ワーカー キャッシュから完全に読み込むことができます。

コンテンツ用アプリ シェル

実用的な実装

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

Service Worker のライフサイクル

アプリケーション シェル プロジェクトでは、次のサービス ワーカーのライフサイクルを提供する sw-precache を使用します。

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

サーバービット

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

サーバーサイドの設定は、デモアプリで使用している設定とは大きく異なる場合があります。このウェブアプリのパターンはほとんどのサーバー設定で実現できますが、一部のアーキテクチャの変更が必要になります。次のモデルが非常に効果的であることがわかりました。

アプリ シェル アーキテクチャの図
  • エンドポイントは、ユーザー向けの URL(インデックス/ワイルドカード)、アプリケーション シェル(サービス ワーカー)、HTML 部分の 3 つの部分で定義されます。

  • 各エンドポイントには、ハンドルバー レイアウトを取得するコントローラがあり、ハンドルバーの部分ビューとビューを取得できます。簡単に言うと、部分ビューは、最終ページにコピーされる HTML のチャンクであるビューです。注: より高度なデータ同期を行う JavaScript フレームワークは、アプリケーション シェル アーキテクチャに移植するのがはるかに簡単です。部分テンプレートではなく、データ バインディングと同期を使用する傾向があります。

  • 最初に、コンテンツを含む静的ページがユーザーに提供されます。このページでは、Service Worker がサポートされている場合は、Service Worker を登録します。Service Worker は、アプリケーション シェルと、それに依存するすべてのもの(CSS、JS など)をキャッシュに保存します。

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

  • Service Worker をサポートしていないブラウザには、常に代替エクスペリエンスを提供する必要があります。このデモでは、基本的な静的サーバーサイド レンダリングにフォールバックしていますが、これは多くのオプションの 1 つにすぎません。サービス ワーカーを使用すると、キャッシュに保存されたアプリケーション シェルを使用して、シングルページ アプリケーション スタイルのアプリのパフォーマンスを向上させる新しい方法が提供されます。

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

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

  • ネットワークを優先し、ネットワークに接続できない場合はキャッシュに保存されたバージョンを使用します。

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

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

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

ツール

Google は、アプリケーションのシェルのプリキャッシュ処理や一般的なキャッシュ パターンの処理を簡単に設定できるように、さまざまなサービス ワーカー ヘルパー ライブラリを維持しています。

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

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

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

アプリに初めてアクセスすると、必要なリソースのセット全体がプリキャッシュされます。これは、アプリストアからネイティブ アプリをインストールする場合と同様です。ユーザーがアプリに戻ると、更新されたリソースのみがダウンロードされます。このデモでは、新しいシェルが利用可能になると、[アプリの更新。新しいバージョンに更新してください」このパターンは、最新バージョンに更新できることをユーザーに知らせるための負担の少ない方法です。

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

sw-toolbox を使用して、リソースに応じてさまざまな戦略でランタイム キャッシュに保存します。

  • イメージの cacheFirst と、N maxEntries のカスタム有効期限ポリシーを持つ専用の名前付きキャッシュ。

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

まとめ

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

テストでは、クライアントとサーバー間でテンプレートを共有し、2 つのアプリケーション レイヤの構築作業を最小限に抑えました。これにより、プログレッシブ エンハンスメントが引き続きコア機能となります。

アプリでサービス ワーカーの使用を検討している場合は、アーキテクチャを確認し、自分のプロジェクトに適しているかどうかを評価してください。

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