はじめに
こんにちは、KAKEHASHIのMusubiInsightチームでエンジニアをしている高田です。
MusubiInsightとは、薬剤師さんの業務データを可視化するBIツールになります。
そんなMusubiInsightにおいて、表示の高速化を狙いにServiceWorkerという技術を導入したので、紹介したいと思います。
MusubiInsightの課題
ServiceWorkerが何か、という話をする前に、MusubiInsightの課題について触れておきます。
ありがたいことに、MusubiInsightのユーザー数は堅調に増加していますが、それに伴いパフォーマンス低下の課題が浮き彫りになってきました。
特に、薬局数や薬剤師数の多い法人様では1リクエストでのAPIの計算量が大きく、なかなかレスポンスが返ってこないために、ユーザー体験を損なってしまっていました。
そこで、チームとしてパフォーマンス改善によるユーザー体験の向上をOKRに掲げ、対策を行うことになりました。
今回紹介する内容はその対策のうちの1つになります。
ServiceWorkerについて
ServiceWorkerとは
ServiceWorkerとは、ブラウザとインターネットの間に介在するプロキシサーバーのようなものです。
ServiceWorkerが一度インストールされると、Webページのバックグラウンドで、Webページとは独立したライフサイクルで動作します。
ではServiceWorkerによって具体的にどんなことができるようになるのかというと、一例ですが次のようなものが挙げられます。
- ブラウザのCacheStorageにHTTPリクエストの結果をキャッシュする
- キャッシュの結果を使ってオフライン環境でも動作する
- 指定したリソースを事前読み込み(Prefetch)する
- Webからのプッシュ通知を受け取る
ServiceWorkerを採用した経緯
MusubiInsightでは、薬剤師さんの業務に関する指標や薬局経営に関する指標、患者の来局状態に関する指標など、指標の目的ごとにページが分かれています。
そのため、ページ遷移ごとにAPIへリクエストが飛び、ユーザーが入力したパラメーターなどをもとにデータ集計が行われる仕様となっています。
また、MusubiInsightはリアルタイム更新ではなく、深夜のバッチ処理で前日分データの一括更新を行っていますが、障害などの緊急時には日中帯であってもバッチの再更新をかけることもあります。
このような特性から、これまでにキャッシュ化の検討も行われてきましたが、キャッシュ事故を防ぐための運用フローが複雑化してしまうため採用していませんでした。
そんな中、MusubiInsightチームで毎週行っている技術勉強会の中で、ServiceWokerについての発表がありました。
そこで、Prefetchや柔軟なキャッシュ操作が可能なServiceWorkerを導入することで、MusubiInsightのパフォーマンス改善につながるのではないかという提案があり、まずは少数ページで実験的に導入していくこととなりました。
MusubiInsightへの導入
全体構成
ServiceWorkerを使用したアプリケーションの全体構成はこのようになっています。
ユーザーからAPIへのリクエストがあると、ServiceWorkerが仲介し、キャッシュの有無を確認したうえで、サーバーへリクエストを流すかキャッシュを返却するかという判断が行われます。
キャッシュには有効期限を設定しており、その有効期限とはバッチの集計完了時刻です。
深夜のバッチ処理ではAirflowを使ったワークフロー処理を行なっていますが、その一番最後の処理が終わった際に、集計完了時刻を格納したファイルをS3へ出力するようにしています。
この集計完了時刻は障害時の再集計でも更新されるので、キャッシュ利用時に集計完了時刻とキャッシュの作成時刻を比較することで、古いキャッシュを使い続けてしまうことを防ぐことができます。
実装
ServiceWorkerは以下のような独自のライフサイクルを持っています。
まず、ライフサイクルの最初のフェーズではブラウザにServiceWorkerを登録します。
ブラウザがServiceWorkerをサポートしているかどうかを確認した後に、ServiceWorkerのスクリプトパスをregisterメソッドに渡しています。
if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register('/sw.js').then( registration => { window['registration'] = registration; }, function (err) { // registration failed console.error('Service Worker 登録失敗', err); }, ); }); }
ServiceWorkerが正常に登録されると、スクリプトがダウンロードされ、ブラウザはServiceWorkerのインストールを開始します。
インストールが完了すると、installイベントが発火し、次のコードが実行されます。
そして、event.waitUntil(sw.skipWaiting());
の記述により、次のフェーズであるアクティベートを試みます。
sw.addEventListener('install', event => { if (event.waitUntil) { event.waitUntil(sw.skipWaiting()); } });
ServiceWorkerがアクティベート状態になると、activeイベントが発行され、次のコードが実行されます。
コード内でServiceWorkerのバージョン管理しており、アクティベート時に旧バージョンでのキャッシュを削除するようにしています。
これにより、ServiceWorkerを更新したのに、古いキャッシュが参照され続けるといったことが起こらないようにしました。
sw.addEventListener('activate', event => { if (!event.waitUntil) { return; } event.waitUntil( // 旧バージョンのキャッシュを削除 caches.keys().then(cacheKeys => { return Promise.all( cacheKeys .filter(cacheKey => { return cacheKey.match(CACHE_NAME_PREFIX) && cacheKey !== CACHE_NAME; }) .map(cacheKey => { return caches.delete(cacheKey); }), ); }), ); });
アクティベートまで完了すると、fetchイベントで、アプリケーションで発行されたAPIリクエストをハンドリングできるようになります。
ServiceWorkerの登録からアクティベートまでは、MusubiInsightの初期ページを表示するタイミングで行われるので、初期ページのリクエスト中にバックグラウンドで動いているServiceWorkerが別ページにリクエストを送りキャッシュを作ることができます。
そして、ユーザーが初期ページからキャッシュ済みのページへ移動する際には、APIリクエストが発生せず、キャッシュのデータを表示するため、非常に高速な表示が可能となっています。
効果
それでは、ServiceWorkerを導入した結果、どの程度速くなったのかを示します。
MusubiInsightでのユーザーの行動は、基本的にログイン→初期ページ→確認したい指標のページに移動、というものになります。
まず、社内検証用のユーザーでServiceWorker導入前に、初期ページからあるページへ遷移した場合のリクエストからレスポンスを受け取るまでの時間がこちら。
サーバー側のキャッシュなどもあるので、多少前後しますが3秒程度でした。
薬局数、薬剤師数が多い場合は、さらにレスポンスタイムは伸びます。
そして、ServiceWorkerを導入し、prefetchを行なった状態で初期ページから該当のページを表示した場合がこちら。
こちらも多少のばらつきはあるものの、導入前よりも圧倒的に高速な表示が可能となりました!
まとめ
本記事では、ServiceWorkerを使ってBIツールの表示高速化の事例を紹介しました。
ServiceWorkerは独自のライフサイクルを持っているなどやや癖があり、キャッチアップが少し大変でしたが、チームメンバーとペアプロしたりしながら、なんとかカタチにすることができました!
結果も良好で満足しています。
今後は、このServiceWorkerを別ページにも横展開していったり、別で走っているパフォーマンス向上施策にも取り組んだり、MusubiInsightでの体験がより良いものになるよう改善を続けていきたいと思います。