Service Worker とアプリケーション シェルモデル

シングルページ ウェブ アプリケーション(SPA)に共通するアーキテクチャ機能は、アプリケーションのグローバルな機能を強化するために必要な最小限の HTML、CSS、JavaScript です。実際には、すべてのページにわたって保持されるヘッダーやナビゲーションなどの一般的なユーザー インターフェース要素がこれに該当します。Service Worker が、この最小限の UI の HTML と依存アセットを事前にキャッシュに保存する場合、これを「アプリケーション シェル」と呼びます。

アプリケーション シェルの図。上部にヘッダー、下部にコンテンツ領域があるウェブページのスクリーンショットです。ヘッダーには「アプリケーション シェル」、下部には「コンテンツ」というラベルが付いています。

Application Shell は、ウェブ アプリケーションのパフォーマンスの認識において重要な役割を果たします。これは最初に読み込まれる要素であり、コンテンツがユーザー インターフェースに表示されるのを待っているユーザーが最初に目にする画面です。

Application Shell の読み込みは速くなりますが(ネットワークが利用可能で、ある程度速い場合)、アプリケーション シェルと関連アセットを事前キャッシュに保存する Service Worker により、Application Shell モデルには次のようなメリットがあります。

  • リピート訪問時の信頼性の高い一貫したパフォーマンス。Service Worker がインストールされていないアプリに初めてアクセスしたとき、Service Worker がアプリのマークアップと関連アセットをキャッシュに保存するには、そのマークアップと関連アセットをネットワークから読み込む必要があります。ただし、アクセスが繰り返されると、アプリケーション シェルがキャッシュから取得されるため、読み込みとレンダリングが即座に行われます。
  • オフライン環境でも機能に確実にアクセス。インターネット アクセスが不安定であったり、まったくアクセスできたりして、「そのウェブサイトが見つかりません」という不安に悩まされることもあります。Application Shell モデルは、ナビゲーション リクエストに対してキャッシュからのアプリケーション シェル マークアップで応答することで、この問題に対処します。ウェブ アプリケーション内のこれまでにアクセスしたことのない URL にユーザーがアクセスした場合でも、アプリケーション シェルはキャッシュから提供され、有用なコンテンツが表示されます。

Application Shell モデルを使用する必要がある場合

ルート間では変わらない一般的なユーザー インターフェース要素があるのに、コンテンツは変更される場合、アプリケーション シェルは最も理にかなっています。ほとんどの SPA は、事実上アプリケーション シェル モデルをすでに使用している可能性があります。

これがご自身のプロジェクトであり、Service Worker を追加して信頼性とパフォーマンスを高める場合、Application Shell は次のことを行う必要があります。

  • 読み込みが速い
  • Cache インスタンスの静的アセットを使用します。
  • ページのコンテンツとは別に、ヘッダーやサイドバーなどの一般的なインターフェース要素を含めます。
  • ページ固有のコンテンツを取得して表示します。
  • 必要に応じて、オフライン再生用に動的コンテンツをキャッシュに保存します。

アプリケーション シェルは、API や JavaScript でバンドルされたコンテンツを介して、ページ固有のコンテンツを動的に読み込みます。また、アプリケーション シェルのマークアップが変更された場合に、Service Worker のアップデートが新しいアプリケーション シェルを取得して自動的にキャッシュするようにもする必要があります。

Application Shell をビルドする

アプリケーション シェルは、コンテンツから独立して存在しつつ、そのシェル内にコンテンツを入力するベースを提供する必要があります。理想的には、可能な限りスリムなコンテンツでありながら、エクスペリエンスの読み込みが速いことをユーザーが理解できるような、意味のあるコンテンツを初回ダウンロードに含めることが理想的です。

適切なバランスはアプリによって異なります。Jake Archibald の Trained To Thrill アプリのアプリケーション シェルには、Flickr から新しいコンテンツを取得するための更新ボタン付きのヘッダーが含まれています。

2 つの異なる状態の Trained to Thrill ウェブアプリのスクリーンショット。左側にはキャッシュされたアプリケーション シェルのみが表示され、コンテンツは入力されていません。右側では、コンテンツ(一部のトレインの画像)がアプリケーション シェルのコンテンツ領域に動的に読み込まれます。

Application Shell のマークアップはプロジェクトによって異なりますが、アプリのボイラープレートを提供する index.html ファイルの例を次に示します。

​​<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>
      Application Shell Example
    </title>
    <link rel="manifest" href="/manifest.json">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="styles/global.css">
  </head>
  <body>
    <header class="header">
      <!-- Application header -->
      <h1 class="header__title">Application Shell Example</h1>
    </header>

    <nav class="nav">
      <!-- Navigation items -->
    </nav>

    <main id="app">
      <!-- Where the application content populates -->
    </main>

    <div class="loader">
      <!-- Spinner/content placeholders -->
    </div>

    <!-- Critical application shell logic -->
    <script src="app.js"></script>

    <!-- Service worker registration script -->
    <script>
      if ('serviceWorker' in navigator) {
        // Register a service worker after the load event
        window.addEventListener('load', () => {
          navigator.serviceWorker.register('/sw.js');
        });
      }
    </script>
  </body>
</html>

ただし、プロジェクト用のアプリケーション シェルを構築する場合、次のような特性が必要です。

  • HTML には、個々のユーザー インターフェース要素ごとに明確に分離された領域が必要です。上の例では、アプリのヘッダー、ナビゲーション、メイン コンテンツ領域、コンテンツの読み込み中にのみ表示される読み込み「スピナー」のスペースが含まれます。
  • アプリケーション シェル用に最初に読み込まれる JavaScript と CSS は最小限に抑え、コンテンツではなくアプリケーション シェル自体の機能にのみ関連している必要があります。これにより、アプリケーションは可能な限り速くシェルをレンダリングし、コンテンツが表示されるまでメインスレッドの処理を最小限に抑えることができます。
  • Service Worker を登録するインライン スクリプト。

Application Shell がビルドされたら、Service Worker をビルドして、そのシェルとそのアセットの両方をキャッシュできます。

アプリケーション シェルのキャッシュ

Application Shell とその必須アセットは、インストール時に Service Worker が直ちに事前キャッシュする必要があるものです。上記の例のようなアプリケーション シェルを想定し、workbox-build を使用する基本的な Workbox の例でこれを実現する方法を見てみましょう。

// build-sw.js
import {generateSW} from 'workbox-build';

// Where the generated service worker will be written to:
const swDest = './dist/sw.js';

generateSW({
  swDest,
  globDirectory: './dist',
  globPatterns: [
    // The necessary CSS and JS for the app shell
    '**/*.js',
    '**/*.css',
    // The app shell itself
    'shell.html'
  ],
  // All navigations for URLs not precached will use this HTML
  navigateFallback: 'shell.html'
}).then(({count, size}) => {
  console.log(`Generated ${swDest}, which precaches ${count} assets totaling ${size} bytes.`);
});

build-sw.js に保存されたこの設定により、アプリの CSS と JavaScript がインポートされます。これには、shell.html に含まれる Application Shell マークアップ ファイルも含まれます。このスクリプトは、次のように Node で実行されます。

node build-sw.js

生成された Service Worker は ./dist/sw.js に書き込まれ、完了すると次のメッセージがログに記録されます。

Generated ./dist/sw.js, which precaches 5 assets totaling 44375 bytes.

ページが読み込まれると、Service Worker は Application Shell マークアップとその依存関係を事前にキャッシュに保存します。

ネットワークからダウンロードしたアセットのリストが表示されている、Chrome の DevTools のネットワーク パネルのスクリーンショット。Service Worker によって事前キャッシュされたアセットは、行の左側に歯車が表示されて他のアセットと区別されます。JavaScript ファイルと CSS ファイルは、インストール時に Service Worker によって事前キャッシュされます。
Service Worker は、インストール時に Application Shell の依存関係を事前キャッシュに保存します。プレキャッシュ リクエストは最後の 2 行です。リクエストの横にある歯車アイコンは、Service Worker がリクエストを処理したことを示します。

Application Shell の HTML、CSS、JavaScript は、バンドラを使用するプロジェクトを含め、ほぼすべてのワークフローで事前キャッシュできます。ドキュメントを進めていくと、Workbox を直接使用してツールチェーンを設定し、SPA であるかどうかに関係なく、プロジェクトに最適な Service Worker をビルドする方法がわかるようになります。

まとめ

Application Shell モデルと Service Worker を組み合わせると、オフライン キャッシュに最適です。特に、マークアップや API レスポンスのために、プリキャッシュ機能とネットワーク ファーストのキャッシュへのフォールバック戦略を組み合わせる場合は特にそうです。その結果、オフライン状態でも、再度アクセスするとアプリケーション シェルが瞬時に表示され、確実な高速エクスペリエンスが実現します。