ServiceWorkerとCacheによるSPAの高速化、オフラインモード
こんにちは
shokaiですshokai.icon
Scrapboxを作っています
横浜の自宅から京都にリモートワークしている
詳細な実装の話
半年ぐらい前の資料
これをアップデートしつつ、もう一度噛み砕いて説明したいshokai.icon
普通のSPAを高速化したり、オフライン対応したりする事を考える どこから手を付けるべきか?
デモ内容
起動がはやい
Desktop PWA
画面遷移がはやい
エディタにいろいろ書ける
オフラインモード
https://gyazo.com/7622d63ce35541453a74a7a53c2196d3
0. ブラウザでページ開くと
1. HTML, JS, CSSなどのassetsをダウンロードして
2. AjaxでAPIからデータをダウンロードして
3. 画面が表示される
よくある高速化
3. 画面が表示されるまで終わったHTMLをいきなり返せば、最初の表示が速くなる
その後で、ブラウザ上でも1,2,3を実行したりする技もある
CDN
1と2のダウンロードが速くなる
どちらも通信を効率化する
往復200 msecぐらいかかるが、問題ない
0. ブラウザでページ開くと
2. 前回取得したAPIデータをCacheStorageから表示
3. 画面が表示される
この時点で操作可能になる
4. AjaxでAPIからデータをダウンロードして
5. 画面がさらに更新される
1, 2, 3まで一切通信をせずブラウザ内のキャッシュでやる
通信するのは4だけ
通信速度を効率化するのではなく、タイミングや順序を入れ替えた
基本の話
プログラマブルなネットワークproxy
HTTP通信を途中で書き換え可能
オフライン表示の為の機能ではない
レスポンスをcacheしてあれば、オフライン表示も実装できるよね(自力でがんばれ)という世界観
何でもできる
通信を握りつぶしたり
送信先を書き換えたり
リクエストしたフリをしてリクエストせず、適当なレスポンスを返したり
回線切ってChrome起動すると見える
https://gyazo.com/438ef8eb64c22ae94b1885b1b9d8fafd
Chromeのホーム画面はServiceWorkerで実装されてるから、オフラインでも表示できる
workerのソースも見れるぞ
KVSです
key Request object
value Response object
最近のブラウザに組み込まれている型
https://gyazo.com/8a41adb085f83eea29f03a8d90513738
UIスレッドとServiceWorkerの両方からアクセスできる
const response = await caches.match(request)で取り出せる
ServiceWorkerのインストール
ここは特に工夫の余地無いので飛ばす
navigator.serviceWorker.registerでググれ
一度インストールすれば、約24時間毎に更新チェックされる
ブラウザが自動的にやってくれる
HTTP通信がServiceWorkerを通る
1. リクエスト
UIスレッド → ServiceWorker → サーバー
2. レスポンス
UIスレッド ← ServiceWorker ← サーバー
どちらもServiceWorkerを通る
Fetchイベント
code: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を返す様にすれば、オフラインモード完成
リンクにマウスホバーで「cache温めといて」
https://gyazo.com/86faa398b233f86be3ad4a15cd2e777e
クリックする前にデータ取得して光速を超える
話を戻す
ServiceWorkerを使った高速化
0. ブラウザでページ開くと
2. 前回取得したAPIデータをCacheStorageから表示
3. 画面が表示される
4. AjaxでAPIからデータをダウンロードして
5. 画面がさらに更新される
順に見ていく
バックグラウンドでHTML, JS, CSSを取得しておく
取得タイミング
ServiceWorkerが自動更新した後
しばらくUIスレッドが通信していない時
日時をkeyにしたCacheStorageに保存してある
https://gyazo.com/550a81a0c38c5da5652dd119a2ab348f
新しいのを取得したら古いのを削除
取得するassetのリスト
まずcacheから返す
cacheに無ければ、networkから取得して返す
2. 前回取得したAPIデータをCacheStorageから表示
ブラウザ側のstateを復元する
この工程にServiceWorkerは関わらない
CacheStorageはUIスレッドからも直接読み書きできるので
stateにreadyState = RESTORE_CACHEをセットしておく
後で使う
エディタの中身はまだこの工程を実装していない
古いページを編集したらややこしくなるので
3. 画面が表示される
stateに基づき、Reactを普通にレンダリングするのだが
UIによっては「あくまでCacheから表示していますよ」と教えた方が良い物もある
https://gyazo.com/43d6a2919ef4c0b1dcebc8ca46fab0cb
最新データの取得&準備にちょっと時間がかかる為
4. AjaxでAPIからデータをダウンロードして
ServiceWorkerは、普通にfetchイベント受けてfetch(request)してresponseをUIスレッドに返す
だけでなく
fetchの失敗をtry-catchして
Cache Storageから返す
responseをCacheStorageに保存しておく
日付を付けて、古いのは消す
外部originの画像もimageに保存
https://gyazo.com/1c581ffc204f3c71bbbe63e3c40c4284
まずnetworkから返そうとする
失敗したらcacheから返す
stateにreadyState = FROM_REMOTEもしくはFALLBACK_CACHEをセットしておく
後で使う
5. 画面がさらに更新される
もう一度Reactのレンダリングを行う
readyState = FROM_REMOTEの時
普通に表示する
ServiceWorkerがインストールされてない場合と同じ
readyState = FALLBACK_CACHEの時
右下にhttps://gyazo.com/d00b202f9ae63fec45d2b45daf1bdac2を表示しつつ
編集系の操作をロックし、閲覧専用にする
Offline mode
Wikiなので編集が行われる
編集した後のデータでCacheStorageを更新したい
色々考えたけど「今見てるページをたまにGETする」が一番簡単だったshokai.icon
まとめ
起動はやい&オフライン表示
ServiceWorker
UIスレッド
stateをまずcacheから復元する
これら3つのstateをユーザーに適切に教える
画面遷移はやい
Scrapboxではこの順で導入した
2. prefetch
3. APIレスポンス全部cacheする
4. オフライン表示
5. cacheから復元して起動速度アップ
6. ページ編集後にcache更新
既存のSPAを高速化するなら、どこからやる?
prefetchだと思うshokai.icon
効果がわかりやすい。画面遷移はしょっちゅうある
実装が一番簡単
起動はやい&オフライン表示 は
実装がちょっとめんどくさい
めんどくさい割に感動が薄い(起動した時しか効果が無い)
ビルド・デプロイシステムにも関わってくる
ブラウザ側でマルチスレッドプログラミングができる
最近はスマホでも8コアとか入ってる
無料でスケールする
API設計が変わってくる
これを
サーバーでデータを計算をしてブラウザに返す
こうしていく
サーバーはデータをドカッと返す
ブラウザで計算する
cacheされたAPIレスポンスを元に、ブラウザ側でインタラクティブな事をやれる
こういう分業になる様にAPIを作っていくと、たぶんオフラインモードでできる事が増える
サーバー
データの単純な保存、アクセス権限チェック
ブラウザ
計算していい感じに表示