HTTP2 Server PushとService Workerを使ったレスポンス改善

Express width HTTP2 Server Push を試したので、
今回はPushされたリソースをCache APIでブラウにキャッシュして、
二度目以降のアクセスはキャッシュを使いHTTPリクエストしないでレスポンスをさらに向上できることを試してみた。

復習でHTTP2 Server Pushを試してみる。

HTTP2 Server Push

サンプルで作ったアプリケーションではStreamにリソースファイルをPushしてレスポンスを返すことで、Server Pushを実現している。

もしアプリケーションの前にNginxなどのProxyサーバーがある場合は、HTTPヘッダーにLinkヘッダーを追加してレスポンスを返すようにしたら良い。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise.all(
pushFiles.map(file => {
const option = Object.assign(stremOption, {'response': {'content-type': file.mime}});
// create push stream
const stream = res.push(file.path, option);

stream.on('error', error => {
console.error(error);
});

stream.end(fs.readFileSync(`${publicPath}${file.path}`));
return file;
})
).then(files => {
next();
});

HTTPリクエストする
(あらかじめcurlでhttp2出来るようにlibcurlにnghttp2オプションを追加している)

1
2
3
4
5
curl -kI https://localhost:3000/push/
HTTP/2 200
x-powered-by: Express
access-control-allow-origin: *
access-control-allow-headers: Origin, X-Requested-With, Content-Type, Accept

Chrome Devtoolのネットワークを見てみると、HTTP2 + Server Push されていることがわかる。

Service Worker

Service Workerはブラウザがwebページのスクリプトとは別のプロセスでスクリプトをバックグラウンドできる機能。
プッシュ通知やモバイルアプリでのバックグラウンドでのデータの同期などが実現できる。

Caniuseでみると各ブラウザのサポート状況はこんな感じ

今回はChrome Canary (v58)を使う。もちろんHTTPS環境は必要。
そのほかChromeでService Workerを使うためのDebug環境を整えておく。

自己証明書を許可する

chromeでローカルホスト証明書を許可するようにフラグを切り替えておく。

chrome://flags/#allow-insecure-localhost を開いて設定を切替る。

Service Worker Inspector

Service Worker が有効になっているかは、chrome://inspect/#service-workers を開くことで確認できる。
Service Worker の一覧と内容、解除を操作できる。

serviceworker-internals

chrome://serviceworker-internals/ を開いて確認できる。

Service Workerの内容や開始、停止やデバックなどを行える。

Service Worker について

まずはService Workerについてざっくりと学ぶ。
Service Workerのライフサイクルは次のようになる。

1. Service Workerの登録

まずページにアクセスすると、Service Workerをダウンロード。
register メソッドをつかってjavascriptを登録する。

1
navigator.serviceWorker.register('/serviceworker.js', {scope: '/'})

登録の際にscopeパラメータオプションを指定できる。
scopeを指定することで、service workerが制御可能なコンテンツを指定できる。
たとえば { socpe: ‘/sw/‘ } と指定した場合、 sw/ ディレクトリ以下をService Workerで制御できる。

ちなみに何度か登録に失敗した、その場合は次のことが考えられる。

  • https通信ではない
  • service workerファイルへのパスが相対パスではない
  • 指定するファイルがSameOriginではない

2 Service Workerのインストールとアクティベート

登録すると、InstallとActivate というイベントが順番に実行される。
すでにService Workerがすでに登録されている場合は、Service Workerが更新できる状況になるまで待つことになる。
(すぐに処理を開始するとデータに不整合が発生する恐れがあるため)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(reg => {
if(reg.installing) {
// インストール中
console.log('Service worker installing');
} else if(reg.waiting) {
// すでに登録されているService Workerがある場合
// service workerが更新できる状況になるまで待つ
console.log('Service worker installed');
} else if(reg.active) {
// アクティベート
console.log('Service worker active');
}
}).catch(err => {
console.error('ServiceWorker registration failed:', err);
});

this.addEventListener('install', event => {
event.waitUntil(
// some install event hook script
);
}, false);

this.addEventListener('activate', event => {
event.waitUntil(
// some event activate hook script
);
}, false);
}

各状態のイベントリスナーを追加することで、callbackできる。
このとき、各遷移状態に移行した際に実行するメソッドは waitUntil()メソッドに引数で渡すことができる。

また、install後に直ちにactivate状態に遷移するには

1
2
3
self.addEventListener('install', event => {
event.waitUntil(self.skipWaiting());
}, false);

activate後に直ちにactivated状態に遷移するには

1
2
3
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
}, false);

今回はinstall時にファイルをキャッシュについする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
const cachePromises = CACHE.map(url => {
const fetchUrl = new URL(url, location.href);
// no-cors: クロスオリジンでのリクエストを防ぐ。HEAD、GET、POST 以外のメソッド実行を防ぐ。
const request = new Request(fetchUrl, { mode: 'no-cors' });

// HTTPリクエストにクレデンシャルを含める
return fetch(request, {credentials: 'include'}).then(response => {
if (response.status >= 400) {
throw new Error('request for ' + fetchUrl + ' failed with status ' + response.statusText);
}
return cache.add(fetchUrl, response);
}).catch(error => {
console.error('Not caching ' + url + ' due to ' + error);
});
});

return Promise.all(cachePromises)

}).catch(error => {
console.error('Pre-fetching failed:', error);
})
);
event.waitUntil(global.skipWaiting());
});

すでにエンドユーザーのブラウザに登録されたservice workerを更新したいときなどに有効な手段っぽい。

3 Fetchイベント

Service Workerがactivateな状態で、scope配下のページを移動したり、ページを更新したりすると、
fetchイベントを受け取る。

このfetchイベントのリスナーでHTTPレスポンスを横取りして、リソースファイルをCache APIを介してに登録したり、読み込んだりできる。
これにより実際にHTTPリクエストすることなくページを返すことができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
self.addEventListener('fetch', event => {
event.respondWith(
// キャッシュの中に該当するリソースがある
caches.match(event.request)
.then(response => {

// キャッシュがあればキャッシュを返す
if (response) {
return response;
}

// リソースを取得する
return fetch(event.request)
.then(response => {
// リソースが取得できないときはレスポンスを返す
// HTTP Statusのチェック
// Same Originのチェック(サードパーティのリソースはキャッシュされない)
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}

// ブラウザに返すレスポンスとキャッシュ用のレスポンスの2つ必要なので複製
const responseToCache = response.clone();

caches.open(CACHE_NAME)
.then(cache => {
// キャッシュを更新
cache.put(event.request, responseToCache);
});

// ブラウザにレスポンスを返す
return response;
}
);
})
);
});

実際にリクエストしてみる。
1回目はHTTP2 + Server Push でレスポンスを返す。

2回目はキャッシュを返す。from ServiceWorkerとあり、リクエストをしないでService WorkerがCacheを返し、ページが描画されることがわかる。

4. Service Worker の更新

Service workerファイルが更新され、ファイルにバイト単位で差分がある場合、
再度ファイルをダウンロード、インストールを行う。

ただし、既存のService Worker稼働しているので、新しくダウンロードしたファイルはwaitingになる。
ページが閉じられるなどして、新しい Service Workerコントーロール可能な状態となりactivateに遷移する。

まとめ

Server Push + Service Worker によりトラフィックは改善される。
両者は技術的にも相性が良いなぁという感じ。
とくにデバイスによってはオフライン時でもキャッシュを利用できることで、恩恵を受けるものもありそう。

また、HTTP2もしくはServie Workerをサポートしてない(両方ともサポートしてない)場合でも副作用はないため、受けられる恩恵は大きい。
まだ市場的には試用段階ではあるが、Progressive Web App(PWA)は今後当たり前にウェブ開発の流れにシフトしていくんだろうな。

作成したサンプル

express-http2-serviceworker-example

参考にしたページ

はじめてのプログレッシブ ウェブアプリ
Service Worker の紹介
ServiceWorker API
Cache
ServiceWorker ready?

Comments