はじめに
Webアプリケーションを開発するにあたってバンドルファイルを分散するためにはdynamic import
を用いてコード分割する必要があります。
この記事ではdynamic import
を用いてコード分割したアプリがファイルを読み込むことができずにエラーを吐いてしまうことに対する解決策を提示します。
なぜ起こるのか
バンドルはwebpackやrollupといったバンドラによって行われます。
これらのバンドラから出荷されるバンドルファイルには内容をハッシュ化した文字列が割り当てられます。
例えばindex.92f37065.js
というファイルには92f37065
とハッシュ値が与えられています。同時に出荷されたファイルに同じハッシュ値が与えられるのではなく、index.92f37065.js
と同時に出荷されたファイルvender.944r675e.js
のようにファイルごとに異なるハッシュ値を持ちます。また、同時に出荷されたファイルはハッシュ名込みでファイルの読み込みなどのやりとりを行います。
ハッシュ値はファイルの内容を元に生成されるので何らかの変更を加えて再生成されたファイルは過去のファイルと異なるハッシュ値を持ちます。
これらを踏まえてユーザーがハッシュ値000
に割り振られたファイルを遅延読み込みするようなファイルを利用しているケースを考えます。
ある日、コードのリリースを行いサーバーではハッシュ値000
のファイルから新たにビルドしたハッシュ値001
のファイルに置き換えされました。
ファイルを置き合えた後もユーザーはリロードをしない限りハッシュ値000
のファイルを遅延読み込みする古いファイルを使い続けてしまうので、リリース後に古いファイルを使い続けているユーザーがファイルを読み込もうとした時にはサーバー側にハッシュ値000
のファイルが存在しない事によってエラーに遭遇していまいます。
この時に発生するエラーが今回議題に挙げたエラーです。
つまりこのエラーはユーザーが持っているファイルのバージョンとサーバーにあるファイルのバージョンが異なることによって起こります。
なお、バンドラによって表示されるエラーが異なることに注意してください。webpackではChunkLoadError
、rollupではTypeError
でFailed to fetch dynamically imported module...
と表示されます。
❌ハッシュ化させない
このエラーはハッシュ化されることが原因なので、ハッシュ化することをやめるという戦略です。
旧ファイルも新ファイルも同じファイル名であるが故に旧ファイルがクライアント側にキャッシュされて新ファイルを読み込んでくれないことや、それによって旧ファイルと新ファイルが組み合わさって動作してしまうなどのバグが生じると考えられますのでこの解決方法は避けるべきです。
❌サーバーに旧ファイルも置く
サーバーに旧ファイルも保管しておくことで、クライアントが持つ旧ファイルから他の旧ファイルを呼び出すことが可能になります。
クライアントがどの時点のファイルを利用しているかが不明なのでどれほど前のファイルを保管しておけば良いのか定まらないことや、旧ファイルのまま実行させることが可能になることで予期せぬ動作だったり通信先の古い情報を扱ってしまったりするので避けた方が良いです。ビルドされるファイルの容量が大きいときはサーバーにも負荷がかかるのでその点でも避けたいです。
⭕️ エラーを検知してリロードさせる
該当のエラーを検知したときにwindow.location.reload
などで強制的にリロードさせたり、リロードを促す画面を表示させる方法です。こちらの技術的な要因でユーザーの操作を阻害してしまうことや、操作を促してしまう点が欠点です。
この戦略でエラーの解消をおこなったプロジェクトでバンドラを変更する場合はエラー検知が正しく行われていることを確認するのを忘れないようにしてください。下のようにwebpackのエラーを正規表現で検知していた場合はrollupへ移行した場合に検知できなくなります。
if (/Loading chunk [\d]+ failed/.test(error.message)) {
window.location.reload();
}
また、強制的にリロードさせる場合はバージョンアップ以外の原因で同じエラーが出た場合はに無限ループに陥るので、回数制限を設けるのを忘れないようにしてください。
⭕️ Service Workerを利用する
Service Workerで一定時間ごとにファイルの更新を監視して、変更を確認した次の遷移をSPAの遷移(pushState)ではなくa
タグの本来の挙動に変更する策です。
a
タグはSPAとは異なり、遷移先のページを一から読み直す挙動をしますので、a
タグの遷移にすることでユーザーが扱うファイルを新しいものに変更する仕組みを作れます。
さらに、Service Workerのキャッシュを確認して存在しなければサーバーから取得させるようなCache Firstの手法をとることで、a
タグの遷移によるパフォーマンスの低下も軽減できます。変更を確認してすぐにService Worker側でファイルを先に読み込んでキャッシュに追加させるのでa
タグの遷移によるファイルの読み込みが軽減されるというわけです。Cache Firstにおけるファイルの取得は以下のような挙動をします。
副次的な効果として、別タブで開いた時などのパフォーマンスも向上させることができることとPWA化のための一歩を歩むことがあります。
この方法には3点のデメリットが考えられます。
- ページ単位の
dynamic import
にしか対応しておらず、他の方法の対策を個別に練らなければいけない点 - 更新を確認する間にファイルの更新と
dynamic import
があった際に防げない点 - 更新を確認するたびにサーバーへ問い合わせる必要があるので通信量が増える点
1点目と2点目はエラーを検知してリロードさせる
も併せて対応することで防ぐことができます。
おわりに
この記事ではユーザーとサーバー間でファイルのバージョンが異なることによって発生するエラーの解決方法を示しました。紹介した解決方法のほとんどはユーザーに新しいバージョンのファイルを利用してもらうアプローチですので、ホットフィックスや新機能を全てのユーザーに確実に提供できるという面でも役に立ちます。
おすすめの対策方法は大規模なアプリケーションではWeb Workerも使った対策、中規模以下のアプリケーションではエラーを受け取ってリロードさせる対策です。