ServiceWorkerとCacheによるSPAの高速化、オフラインモード
こんにちは
shokaiです
Scrapboxを作っています
横浜の自宅から京都にリモートワークしている
詳細な実装の話
半年ぐらい前の資料
これをアップデートしつつ、もう一度噛み砕いて説明したい
普通の
SPAを高速化したり、オフライン対応したりする事を考える
どこから手を付けるべきか?
デモ内容
起動がはやい
Desktop PWA
画面遷移がはやい
エディタにいろいろ書ける
オフラインモード
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起動すると見える
Chromeのホーム画面はServiceWorkerで実装されてるから、オフラインでも表示できる
workerのソースも見れるぞ
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.jsself.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温めといて」
クリックする前にデータ取得して光速を超える
話を戻す
ServiceWorkerを使った高速化
0. ブラウザでページ開くと
2. 前回取得したAPIデータをCacheStorageから表示
3. 画面が表示される
4. AjaxでAPIからデータをダウンロードして
5. 画面がさらに更新される
順に見ていく
バックグラウンドでHTML, JS, CSSを取得しておく
取得タイミング
ServiceWorkerが自動更新した後
しばらくUIスレッドが通信していない時
日時をkeyにしたCacheStorageに保存してある
新しいのを取得したら古いのを削除
取得するassetのリスト
まず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から返そうとする
失敗したらcacheから返す
stateに readyState = FROM_REMOTE
もしくは FALLBACK_CACHE
をセットしておく
後で使う
5. 画面がさらに更新される
もう一度Reactのレンダリングを行う
readyState = FROM_REMOTE
の時
普通に表示する
ServiceWorkerがインストールされてない場合と同じ
readyState = FALLBACK_CACHE
の時
右下に
を表示しつつ
編集系の操作をロックし、閲覧専用にする
Offline mode
Wikiなので編集が行われる
編集した後のデータでCacheStorageを更新したい
色々考えたけど「今見てるページをたまにGETする」が一番簡単だった
まとめ
起動はやい&オフライン表示
ServiceWorker
UIスレッド
stateをまずcacheから復元する
これら3つのstateをユーザーに適切に教える
画面遷移はやい
Scrapboxではこの順で導入した
2. prefetch
3. APIレスポンス全部cacheする
4. オフライン表示
5. cacheから復元して起動速度アップ
6. ページ編集後にcache更新
既存のSPAを高速化するなら、どこからやる?
prefetchだと思う
効果がわかりやすい。画面遷移はしょっちゅうある
実装が一番簡単
起動はやい&オフライン表示 は
実装がちょっとめんどくさい
めんどくさい割に感動が薄い(起動した時しか効果が無い)
ビルド・デプロイシステムにも関わってくる
ブラウザ側でマルチスレッドプログラミングができる
最近はスマホでも8コアとか入ってる
無料でスケールする
API設計が変わってくる
これを
サーバーでデータを計算をしてブラウザに返す
こうしていく
サーバーはデータをドカッと返す
ブラウザで計算する
cacheされたAPIレスポンスを元に、ブラウザ側でインタラクティブな事をやれる
こういう分業になる様にAPIを作っていくと、たぶんオフラインモードでできる事が増える
サーバー
データの単純な保存、アクセス権限チェック
ブラウザ
計算していい感じに表示