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

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

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

アプリケーション シェルは、ウェブ アプリケーションで認識されるパフォーマンスに大きな影響を与えます。読み込みは最初に読み込まれるので、コンテンツがユーザー インターフェースに表示されるのを待つ間、ユーザーが最初に目にするものでもあります。

アプリケーション シェルの読み込みは高速ですが(ネットワークが利用可能で、少なくともある程度高速であれば)、Service Worker でアプリケーション シェルとその関連アセットをプリキャッシュすることで、アプリケーション シェルのモデルに次のようなメリットももたらされます。

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

どのような場合にアプリケーション シェル モデルを使用すべきか

アプリケーション シェルは、ルート間で変更されない共通のユーザー インターフェース要素があるが、コンテンツは変更されている場合に最適です。ほとんどの SPA は、すでに実質的にはアプリケーション シェル モデルを使用しています。

ご自分のプロジェクトがこれに該当し、その信頼性とパフォーマンスを高めるために Service Worker を追加したい場合、アプリケーション シェルで次のことを行う必要があります。

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

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

アプリケーション シェルのビルド

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

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

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

アプリケーション シェルのマークアップはプロジェクトによって異なりますが、アプリケーションのボイラープレートを提供する 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 を登録するインライン スクリプト。

アプリケーション シェルをビルドしたら、Service Worker をビルドして、そのアセットとそのアセットの両方をキャッシュに保存できます。

アプリケーション シェルをキャッシュする

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

// 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 に保存されているこの構成は、shell.html に含まれるアプリケーション シェル マークアップ ファイルを含め、アプリの CSS と JavaScript をインポートします。このスクリプトは、次のように Node で実行されます。

node build-sw.js

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

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

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

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

アプリケーション シェルの HTML、CSS、JavaScript の事前キャッシュは、バンドラを使用するプロジェクトを含め、ほぼすべてのワークフローで可能です。ドキュメントを読み進めながら、Workbox を直接使用してツールチェーンを設定し、SPA であるかどうかに関係なく、プロジェクトに最適な Service Worker をビルドする方法を紹介します。

まとめ

オフライン キャッシュには、Application Shell モデルと Service Worker を組み合わせると効果的です。特に、事前キャッシュ機能と、マークアップや API レスポンスのネットワークファースト、キャッシュへのフォールバック戦略を組み合わせる場合に効果的です。その結果、オフラインの状態でも、再訪問時にアプリケーション シェルが即座にレンダリングされる、信頼性に優れた高速ユーザー エクスペリエンスを実現できます。