ナビゲーション プリロードを使用すると、リクエストを並行して行うことで、サービス ワーカーの起動時間を短縮できます。
概要
- 状況によっては、サービス ワーカーの起動時間がネットワーク レスポンスを遅らせることがあります。
- 3 つの主要なブラウザ エンジンで利用可能なナビゲーション プリロードは、サービス ワーカーの起動と並行してリクエストを行うことで、この問題を解決します。
- ヘッダーを使用してプリロード リクエストと通常のナビゲーションを区別し、異なるコンテンツを配信できます。
問題
サービス ワーカーを使用して fetch イベントを処理するサイトに移動すると、ブラウザはサービス ワーカーにレスポンスを要求します。これには、サービス ワーカーの起動(まだ実行されていない場合)と fetch イベントのディスパッチが含まれます。
起動時間はデバイスと条件によって異なります。通常は 50 ミリ秒程度です。モバイルでは 250 ミリ秒程度です。極端な場合(デバイスの動作が遅い、CPU の負荷が高いなど)は 500 ミリ秒を超えることがあります。ただし、サービス ワーカーはイベント間のブラウザで決定された時間だけ起動しているため、この遅延が発生するのは、ユーザーが新しいタブや別のサイトからサイトに移動した場合など、まれにしか発生しません。
キャッシュから応答する場合は、ネットワークをスキップするメリットが起動の遅延よりも大きいため、起動時間は問題になりません。ただし、ネットワークを使用して応答している場合は…
サービス ワーカーの起動により、ネットワーク リクエストが遅延しています。
Google では、V8 でのコード キャッシュの使用、fetch イベントのないサービス ワーカーのスキップ、サービス ワーカーの投機的起動などの最適化により、起動時間の短縮を継続しています。ただし、起動時間は常に 0 より大きくなります。
Facebook は、この問題の影響を Google に伝え、ナビゲーション リクエストを並行して実行する方法を尋ねました。
ナビゲーションのプリロード
ナビゲーション プリロードは、「ユーザーが GET ナビゲーション リクエストを行ったときに、サービス ワーカーの起動中にネットワーク リクエストを開始する」という処理を可能にする機能です。
起動の遅延は残りますが、ネットワーク リクエストがブロックされないため、ユーザーはコンテンツをより早く取得できます。
次の動画は、サービス ワーカーに while ループを使用して 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
イベントでこれを使用する必要があるため、サービス ワーカーの 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
で解決されます。
プリロードのカスタム レスポンス
ページでネットワークからのデータが必要な場合、最も迅速な方法は、サービス ワーカーでデータをリクエストし、キャッシュからの部分とネットワークからの部分を含む 1 つのストリーミング レスポンスを作成することです。
記事を表示したいとします。
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
は、各リクエストのストリームをマージする小さな関数です。つまり、ネットワーク コンテンツがストリーミングされる間、キャッシュに保存されたヘッダーを表示できます。
ネットワーク リクエストがページ リクエストと同時に行われ、大きなハックなしでコンテンツをストリーミングできるため、「アプリシェル」モデルよりも高速です。
ただし、includeURL
のリクエストは、サービス ワーカーの起動時間だけ遅延します。ナビゲーション プリロードを使用してもこの問題を解決できますが、この場合はページ全体をプリロードするのではなく、インクルードをプリロードします。
これをサポートするため、プリロード リクエストごとにヘッダーが送信されます。
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 に感謝します。標準化の取り組みに関わったすべての人に感謝します。