Service Worker で横取りしたリクエストをネットワークにフォールバックさせたい時はただ return するべき

タイトルの通り、[Service Worker](http://www.html5rocks.com/ja/tutorials/service-worker/introduction/) の [FetchEvent](https://developer.mozilla.org/ja/docs/Web/API/FetchEvent) で横取りしたリクエストをネットワークにフォールバックさせたい場合は return するだけで OK です。

```js:sw.js
self.AddEventListener("fetch", function(event) {
  return;  // network fallback happens
});
```

次のように明示的に fetch して ```respondWith()``` で返す必要はありません。むしろ fetch したレスポンスが一旦 Service Worker スクリプトに渡されるため、ただ return するよりも処理が遅くなります。

```js:sw.js
self.AddEventListener("fetch", function(event) {
  event.respondWith(fetch(event.request));
});
```

以後、ただ return する場合を "**return-fallback**"、fetch して ```respondWith()``` で返す場合を "**fetch-fallback**" と表記します。

# ベンチマーク

fetch-falback だと処理が遅くなることを確認するため、簡単なベンチマークを実施してみました。次のコードは 2048 bytes のファイルを 100 回リクエストします。リクエストは Service Worker によって横取りされますが、return-fallback もしくは fetch-fallback でネットワークにフォールバックされます。

```js:sw.js
self.AddEventListener("fetch", function(event) {
  // return;
  //   OR
  // event.respondWith(fetch(event.request));
});
```

```js:index.html
var start_time = 0;
navigator.serviceWorker.ready.then(function() {
      var promises = [];
      start_time = performance.now();
      for (var i = 0; i < 100; ++i)
        promises.push(fetch("2048-bytes.txt"));
      return Promise.all(promises);
    })
  .then(function() {
      console.log(performane.now() - start_time);
    });
```

これを Macbook Air (OS X 10.10, 1.7 GHz Intel Core i7, 8GB RAM) の Chrome 49.0.2568.0 で、それぞれ 5 回ずつ実行して計測してみました。サーバは localhost に立て、あらかじめ何回か実行して HTTP Cache や Service Worker をウォームアップしてから計測しています。フォールバックしたリクエストは HTTP Cache から返ります。結果は次の表の通りです。

|    |return-fallback [ms]|respondWith(fetch) [ms]|
|:---|-------------------:|----------------------:|
|1   |             125.985|                207.890|
|2   |             132.205|                238.600|
|3   |             119.090|                200.055|
|4   |             136.850|                197.080|
|5   |             118.105|                202.100|
|avg |             126.447|                209.145|

return-fallback だと平均 126 ms、fetch-fallback だと平均 209 ms かかりました。このことから、**リクエストをネットワークにフォールバックさせたい場合は ```respondWith(fetch(request))``` で返すのではなく、ただ return した方が処理が早い**ことが分かります。

# (おまけ) Chrome ブラウザの内部実装の話

「レスポンスが一旦 Service Worker スクリプトに渡される」とどうして遅くなるかというと、(1) ネットワーク処理を行うプロセス (Browser プロセス) と Service Worker のプロセスの間でプロセス間通信が発生するのと、(2) Service Worker のスクリプトにレスポンスが渡ると C++ と V8 の間で実行コンテキストが切り替わるからです。

![current-data-flow.png](https://qiita-image-store.s3.amazonaws.com/0/426/de34231f-7539-7d6e-3c40-f3fdaba4bce2.png)

Chrome では ```event.respondWith(fetch(event.request))``` というコードがあった場合には Service Worker プロセスをバイパスし、プロセス間通信やコンテキスト切り替えのオーバヘッドを取り除く最適化を入れるのはどうか、という提案が出されています ([crbug](https://code.google.com/p/chromium/issues/detail?id=450471))。しかし、return-fallback によってこれを回避でき、また最適化を入れるには複雑な変更が必要となるため、まだ実装されていません。

![optimized-data-flow.png](https://qiita-image-store.s3.amazonaws.com/0/426/d28d8e3f-1188-acee-169c-2c842b45ae88.png)

# 謝辞

この記事の草稿を [@kinu](https://twitter.com/kinu) さんと [@horo](https://twitter.com/horo) さんにチェックしてもらいました。ありがとうございます。

---

DISCLAIMER: ここで述べられている内容はすべて私の個人的な意見に基づくものであり、所属する組織、団体とは一切関係ありません。