ナビゲーションのプリロードを使用すると、リクエストを並行して行うことで、Service Worker の起動時間を短縮できます。
まとめ
- 状況によっては、Service Worker の起動時間がネットワーク レスポンスの遅れになることがあります。
- 3 つの主要なブラウザエンジンで利用できるナビゲーション プリロードは、Service Worker の起動と並行してリクエストを実行できるようにすることで、この問題を修正しています。
- ヘッダーを使用して、プリロード リクエストと通常のナビゲーションを区別し、異なるコンテンツを提供できます。
問題
Service Worker を使用してフェッチ イベントを処理するサイトに移動すると、ブラウザは Service Worker にレスポンスを要求します。これには、Service Worker を起動し(まだ実行していない場合)、fetch イベントをディスパッチする必要があります。
起動時間はデバイスと条件によって異なります。通常は 50 ミリ秒前後です。モバイルでは 250 ミリ秒ほどです。極端な場合(低速のデバイス、CPU が負荷がかかった状態)では、500 ミリ秒を超えることもあります。ただし、Service Worker はブラウザによって決められたイベント間隔で起動したままになるため、この遅延が発生するのはまれです。たとえば、ユーザーが新しいタブからサイトに移動した場合や、別のサイトに移動した場合などです。
キャッシュから応答する場合は、起動時間よりもネットワークをスキップする利点の方が大きいため、起動時間は問題になりません。ネットワークを使用して応答する場合は...
Service Worker の起動によってネットワーク リクエストが遅延する。
Google は、V8 のコード キャッシュの使用、fetch イベントのない Service Worker のスキップ、Service Worker の投機的な起動、その他の最適化により、起動時間の短縮を続けています。ただし、起動時間は常にゼロより長くなります。
Facebook はこの問題の影響について注意喚起し、ナビゲーション リクエストを並行して実行する方法を求めていました。
ナビゲーション プリロードによる問題解決
ナビゲーションのプリロードは、「ユーザーが GET ナビゲーション リクエストを行ったときに、Service Worker の起動中にネットワーク リクエストを開始する」という動作を指示できる機能です。
起動遅延は生じますが、ネットワーク リクエストはブロックされないため、ユーザーはコンテンツをより早く表示できます。
その動作を示す動画を次に示します。ここでは、while ループを使用して Service Worker に 500 ミリ秒の起動遅延が意図的に設定されています。
こちらはデモ自体です。ナビゲーションのプリロードを利用するには、この機能をサポートしているブラウザが必要です。
ナビゲーションのプリロードを有効にする
addEventListener('activate', event => {
event.waitUntil(async function() {
// Feature-detect
if (self.registration.navigationPreload) {
// Enable navigation preloads!
await self.registration.navigationPreload.enable();
}
}());
});
いつでも navigationPreload.enable()
を呼び出せます。navigationPreload.disable()
で無効にすることもできます。ただし、fetch
イベントでこれを使用する必要があるため、Service Worker の activate
イベントで有効と無効を切り替えることをおすすめします。
プリロードされたレスポンスの使用
これで、ブラウザがナビゲーションのプリロードを実行しますが、引き続きレスポンスを使用する必要があります。
addEventListener('fetch', event => {
event.respondWith(async function() {
// Respond from the cache if we can
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
// Else, use the preloaded response, if it's there
const response = await event.preloadResponse;
if (response) return response;
// Else try the network.
return fetch(event.request);
}());
});
event.preloadResponse
は、次の場合にレスポンスで解決される Promise です。
- ナビゲーションのプリロードは有効になっています。
- リクエストが
GET
リクエストである。 - リクエストがナビゲーション リクエスト(iframe を含むページの読み込み時にブラウザによって生成される)である。
それ以外の場合、event.preloadResponse
はまだ存在しますが、undefined
で解決されます。
プリロードに対するカスタム レスポンス
ページにネットワークからのデータが必要な場合は、Service Worker でリクエストし、キャッシュの各部分とネットワークからの部分を含む単一のストリーミング レスポンスを作成するのが最も簡単な方法です。
ある記事を表示するとします。
addEventListener('fetch', event => {
const url = new URL(event.request.url);
const includeURL = new URL(url);
includeURL.pathname += 'include';
if (isArticleURL(url)) {
event.respondWith(async function() {
// We're going to build a single request from multiple parts.
const parts = [
// The top of the page.
caches.match('/article-top.include'),
// The primary content
fetch(includeURL)
// A fallback if the network fails.
.catch(() => caches.match('/article-offline.include')),
// The bottom of the page
caches.match('/article-bottom.include')
];
// Merge them all together.
const {done, response} = await mergeResponses(parts);
// Wait until the stream is complete.
event.waitUntil(done);
// Return the merged response.
return response;
}());
}
});
上記の例では、mergeResponses
は各リクエストのストリームを結合する小さな関数です。つまり、ネットワーク コンテンツがストリーミングされる間、キャッシュされたヘッダーを表示できます。
この方法は「App Shell」モデルよりも迅速です。ページ リクエストと同時にネットワーク リクエストも行われるため、大規模なハッキングを行うことなくコンテンツをストリーミングできます。
ただし、includeURL
のリクエストは Service Worker の起動時間によって遅延します。ナビゲーション プリロードを使用してこの問題を解決することもできますが、この場合はページ全体をプリロードするのではなく、インクルードをプリロードします。
これをサポートするために、プリロード リクエストごとにヘッダーが送信されます。
Service-Worker-Navigation-Preload: true
サーバーはこれを使用して、ナビゲーション プリロード リクエストに対して、通常のナビゲーション リクエストとは異なるコンテンツを送信できます。キャッシュでレスポンスの相違を認識できるように、必ず Vary: Service-Worker-Navigation-Preload
ヘッダーを追加してください。
これで、プリロード リクエストを使用できるようになりました。
// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
// Else do a normal fetch
.then(r => r || fetch(includeURL))
// A fallback if the network fails.
.catch(() => caches.match('/article-offline.include'));
const parts = [
caches.match('/article-top.include'),
networkContent,
caches.match('/article-bottom')
];
ヘッダーを変更する
デフォルトでは、Service-Worker-Navigation-Preload
ヘッダーの値は true
ですが、任意の値に設定できます。
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
console.log('Done!');
});
たとえば、ローカルにキャッシュした最後の投稿の ID を設定すると、サーバーは新しいデータのみを返すようになります。
状態の取得
ナビゲーションのプリロードの状態は、getState
を使用して確認できます。
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.getState();
}).then(state => {
console.log(state.enabled); // boolean
console.log(state.headerValue); // string
});
この機能に協力してくださった Matt Falkenhagen 氏と Tsuyoshi Horo 氏に感謝いたします。また、この記事もありがとうございました。そして、標準化の取り組みに関わったすべての皆様に深く感謝いたします。