generated at
ServiceWorkerとCacheによるSPAの高速化、オフラインモード

Cosenseの中で、ServiceWorkerを使って何やってるか解説する


こんにちは
shokaiですshokai
Scrapboxを作っています
横浜の自宅から京都にリモートワークしている
福岡のほうが京都より安いし近い✈️


詳細な実装の話
半年ぐらい前の資料
これをアップデートしつつ、もう一度噛み砕いて説明したいshokai

普通のSPAを高速化したり、オフライン対応したりする事を考える
どこから手を付けるべきか?


デモ内容
起動がはやい
Desktop PWA
画面遷移がはやい
エディタにいろいろ書ける
オフラインモード


普通のSPAの動作
0. ブラウザでページ開くと
1. HTML, JS, CSSなどのassetsをダウンロードして
2. AjaxでAPIからデータをダウンロードして
3. 画面が表示される

よくある高速化
3. 画面が表示される まで終わったHTMLをいきなり返せば、最初の表示が速くなる
その後で、ブラウザ上でも1,2,3を実行したりする技もある
CDN
1と2のダウンロードが速くなる
どちらも通信を効率化する

これらはCosenseでは全くやっていない
サーバーはアメリカのHerokuにある
往復200 msecぐらいかかるが、問題ない

ServiceWorkerで高速化
0. ブラウザでページ開くと
1. HTML, JS, CSSなどをCacheStorageから表示
2. 前回取得したAPIデータをCacheStorageから表示
3. 画面が表示される
この時点で操作可能になる
4. AjaxでAPIからデータをダウンロードして
5. 画面がさらに更新される

1, 2, 3まで一切通信をせずブラウザ内のキャッシュでやる
通信するのは4だけ
通信速度を効率化するのではなく、タイミングや順序を入れ替えた


基本の話

ServiceWorkerとは?
プログラマブルなネットワークproxy
HTTP通信を途中で書き換え可能
オフライン表示の為の機能ではない
レスポンスをcacheしてあれば、オフライン表示も実装できるよね(自力でがんばれ)という世界観
何でもできる
通信を握りつぶしたり
送信先を書き換えたり
リクエストしたフリをしてリクエストせず、適当なレスポンスを返したり

回線切ってChrome起動すると見える
Chromeのホーム画面はServiceWorkerで実装されてるから、オフラインでも表示できる
workerのソースも見れるぞ

CacheStorageとは?
KVSです
key Request object
value Response object
最近のブラウザに組み込まれている型
UIスレッドとServiceWorkerの両方からアクセスできる
const response = await caches.match(request) で取り出せる

ServiceWorkerのインストール
ここは特に工夫の余地無いので飛ばす
navigator.serviceWorker.register でググれ
一度インストールすれば、約24時間毎に更新チェックされる
ブラウザが自動的にやってくれる

HTTP通信がServiceWorkerを通る
1. リクエスト
UIスレッド → ServiceWorker → サーバー
2. レスポンス
UIスレッド ← ServiceWorker ← サーバー
どちらもServiceWorkerを通る

Fetchイベント
serviceworker.js
self.addEventListener('fetch', event => { event.respondWith((async () => { const response = await fetch(event.request) return response })()) })
1. fetchというイベントが来る
2. 関数の方の fetch(request) でサーバーから取得して
3. UIスレッドに返す
これを自由に拡張して
例えば fetch(request) が失敗したらcacheを返す様にすれば、オフラインモード完成

ServiceWorkerとUIスレッドの間は、postMessageでもやりとりできる
リンクにマウスホバーで「cache温めといて」
クリックする前にデータ取得して光速を超える



話を戻す

ServiceWorkerを使った高速化
0. ブラウザでページ開くと
1. HTML/JS/CSSをCacheStorageから表示
2. 前回取得したAPIデータをCacheStorageから表示
3. 画面が表示される
4. AjaxでAPIからデータをダウンロードして
5. 画面がさらに更新される

順に見ていく


1. HTML, JS, CSSなどをCacheStorageから表示
assets cacheという仕組みを実装した
バックグラウンドでHTML, JS, CSSを取得しておく
取得タイミング
ServiceWorkerが自動更新した後
しばらくUIスレッドが通信していない時
日時をkeyにしたCacheStorageに保存してある
新しいのを取得したら古いのを削除
取得するassetのリスト

このServiceWorkerの振る舞いをcache firstと呼んでいる
まずcacheから返す
cacheに無ければ、networkから取得して返す

2. 前回取得したAPIデータをCacheStorageから表示
ブラウザ側のstateを復元する
この工程にServiceWorkerは関わらない
CacheStorageはUIスレッドからも直接読み書きできるので
stateに readyState = RESTORE_CACHE をセットしておく
後で使う

エディタの中身はまだこの工程を実装していない
古いページを編集したらややこしくなるので

3. 画面が表示される
stateに基づき、Reactを普通にレンダリングするのだが
UIによっては「あくまでCacheから表示していますよ」と教えた方が良い物もある
最新データの取得&準備にちょっと時間がかかる為

4. AjaxでAPIからデータをダウンロードして
ServiceWorkerは、普通にfetchイベント受けて fetch(request) してresponseをUIスレッドに返す
だけでなく
fetchの失敗をtry-catchして
Cache Storageから返す
responseをCacheStorageに保存しておく
日付を付けて、古いのは消す
外部originの画像もimageに保存
これをnetwork first (cache second)と呼んでいる
まずnetworkから返そうとする
失敗したらcacheから返す
stateに readyState = FROM_REMOTE もしくは FALLBACK_CACHE をセットしておく
後で使う

5. 画面がさらに更新される
もう一度Reactのレンダリングを行う
readyState = FROM_REMOTE の時
普通に表示する
ServiceWorkerがインストールされてない場合と同じ
readyState = FALLBACK_CACHE の時
右下にを表示しつつ
編集系の操作をロックし、閲覧専用にする
これでOffline modeができたshokaishokaishokaishokaishokai

Offline mode
Wikiなので編集が行われる
編集した後のデータでCacheStorageを更新したい
色々考えたけど「今見てるページをたまにGETする」が一番簡単だったshokai



まとめ
起動はやい&オフライン表示
ServiceWorker
assetsをcache firstで返す
APIリクエストをnetwork firstで処理する
UIスレッド
stateをまずcacheから復元する
これら3つのstateをユーザーに適切に教える
画面遷移はやい

Scrapboxではこの順で導入した
1. assets cache実装して
2. prefetch
3. APIレスポンス全部cacheする
4. オフライン表示
5. cacheから復元して起動速度アップ
6. ページ編集後にcache更新

既存のSPAを高速化するなら、どこからやる?
prefetchだと思うshokai
効果がわかりやすい。画面遷移はしょっちゅうある
実装が一番簡単
起動はやい&オフライン表示 は
assets cacheがまず必要
実装がちょっとめんどくさい
めんどくさい割に感動が薄い(起動した時しか効果が無い)
ビルド・デプロイシステムにも関わってくる
rakusaiが作った https://github.com/nota/sw_skelton が参考になる



WebWorkerも併用するとアツい
ブラウザ側でマルチスレッドプログラミングができる
最近はスマホでも8コアとか入ってる
無料でスケールする
API設計が変わってくる
これを
サーバーでデータを計算をしてブラウザに返す
こうしていく
サーバーはデータをドカッと返す
ブラウザで計算する
cacheされたAPIレスポンスを元に、ブラウザ側でインタラクティブな事をやれる
こういう分業になる様にAPIを作っていくと、たぶんオフラインモードでできる事が増える
サーバー
データの単純な保存、アクセス権限チェック
ブラウザ
計算していい感じに表示