EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

Angular アプリケーションで ChunkLoadError をハンドリングする

こんにちはあるいはこんばんは。フロントエンドエンジニアの id:kasaharu です。

エモーションテックでは Angular を使ってアプリケーション開発をしています。今回は Angular アプリケーションで ChunkLoadError をハンドリングする対応をおこなったので、それについて紹介します。

遅延ロードと ChunkLoadError

Single Page Application (SPA) を開発をする際に、アプリケーションの開発が進んでくると初期バンドルのサイズも大きくなっていくため、遅延ロードを検討することがよくある。Angular でもルーティング単位で簡単に遅延ロードする仕組みが提供されている。

Lazy-loading feature modules • Angular

(最新の Angular ではコンポーネント単位でも遅延ロードできる Deferrable Views ���いう機能が提供されているが、今回は話の対象外とする)

遅延ロードをすることで初期のバンドルサイズを小さくすることができる。初期バンドルに含まれなかった JS はチャンク分割され、必要になったタイミングでロードされる。

しかしこのチャンク分割とアプリケーションの更新によって ChunkLoadError というエラーが発生する場合がある。

ChunkLoadError が発生する仕組み

弊社のアプリケーションは build 後の assets を S3 に配置し、CloudFront 経由で配信している。 これを前提として ChunkLoadError が発生する仕組みを説明する。

例えば次のようなアプリケーションがあるとする。

(※ 以下のサンプルは https://angular.jp/guide/example-apps-list#lazy-loading-ngmodules をベースとする)

  • アプリケーションは / にアクセスしたときに HomeComponent を表示する
  • Customers を押すと /customers に遷移し、CustomersComponent を表示する
  • CustomersComponent 用の JS はチャンク分割されおり、表示のタイミングで取得する

ここにデプロイが絡むことで以下のような振る舞いになる。

(以下の説明に出てくるファイル名はすべて仮のものである)

最初のデプロイでアプリケーション ver.1 が公開される。S3 には index.html, main.ver1.js および chunk-customers.ver1.js が配置される。

ユーザーがトップページを開くと、ブラウザは index.html と main.ver1.js を取得する。このとき chunk-customers.ver1.js は取得していないが、main.ver1.js により /customers を開くときに chunk-customers.ver1.js が必要になることを知っている状態になる。

ソースコードに変更が入り、デプロイがおこなわれるとアプリケーション ver.2 が公開される。S3 には index.html, main.ver2.js および chunk-customers.ver2.js が配置される。

先ほどデプロイ前にトップページを開いていたユーザーが /customers を開くとブラウザは customers.ver1.js を取得しに行くが、S3 にはすでに chunk-customers.ver1.js がないためエラーになる。

このときのエラーが ChunkLoadError である。

課題点

今回課題となったのは次の 2 点である。

  • アプリケーションを操作しているユーザーはエラーが発生したことに気づかない
    • 経験値の高いユーザーが、なんとなくブラウザを再読み込みすることで復帰している状態
  • 発生したエラーがエラートラッカーに投げられアラートとして通知される

対策案

対策をするにあたり、いくつか案を検討したのでまずはそれを紹介する。案は以下の 3 つである。

案 1: ChunkLoadError をハンドリングする

1 つ目の案はエラー自体は受け入れ、ハンドリングする方法である。これにより、ユーザーに適切にフィードバックしつつ、エラートラッカーに投げるのも止めることができる。

  • メリット
    • 正しくハンドリングすれば、対応コストは小さい
  • デメリット
    • エラー自体は発生してしまう

案 2: 数世代前の assets も配信する

2 つ目は数世代前の assets も配信することである。これにより、そもそも対象の assets が存在しないことがなくなり、エラーそのものが発生しなくなる。

  • メリット
    • ChunkLoadError が発生しなくなる
  • デメリット
    • 数世代の assets を配信できるようにインフラ構成を変更する必要がある
    • ひとつの assets の生存期間が長くなるため API のバージョン乖離も気にする必要がある

案 3: 遅延ロードしない

最後は遅延ロードしない、である。遅延ロードしなければチャンク分割も発生しないため始めから問題が起きない。

  • メリット
    • ChunkLoadError が発生しなくなる
  • デメリット
    • アプリケーションが大きくなるほど、初期バンドルが大きくなりパフォーマンスが悪化するという最初の問題が残る

採用した案と技術的な詳細

今回は、エラーに気付けないユーザーを最速で救うために、コスパのよい案 1 を採用した。

ここでもう一つ問題が発生する。弊社は Nx を使った monorepo の構成でアプリケーションを開発しており、プロジェクトを開始した時期にばらつきがある。

Angular は v17 で esbuild ベースの builder がデフォルトとなっているがそれ以前は webpack ベースであり、弊社にはその両方のプロジェクトが存在する。

そのため、2 つのパターンの実装が必要となった。以下でそれぞれを紹介する。

webpack を使ってビルドするプロジェクト

webpack でビルドされた module の ChunkLoadError は error.name が 'ChunkLoadError' であるかを確認するのが一番早い。そのため CustomErrorHandler で error.name を確認し 'ChunkLoadError' の場合はユーザーにリロードを促すメッセージを表示しつつ、エラートラッカーには送信しないようにする。

実装する際には以下を参考にした。

webpack/lib/web/JsonpChunkLoadingRuntimeModule.js at v5.89.0 · webpack/webpack · GitHub

コードは次の通りである。

if (error instanceof Error) {
  const chunkLoadErrorName = /ChunkLoadError/;
  if (chunkLoadErrorName.test(error.name)) {
    window.alert('アプリケーションが更新されました。ページをリロードしてください。');
    return;
  }
}

esbuild を使ってビルドするプロジェクト

次に esbuild ベースのプロジェクトについて考える。

esbuild でビルドされた module の ChunkLoadError は error.type が 'TypeError' となる。'TypeError' は ChunkLoadError 以外でも発生するため、error.type に加え error.message も確認してハンドリングする。

なお error.message はブラウザによって異なる。弊社のプロダクトがサポートする動作環境は Chrome, Edge, Firefox, Safari であるため、それぞれのエラーメッセージを以下に示す。

  • Chrome, Edge
    • 'Failed to fetch dynamically imported module'
  • Firefox
    • 'error loading dynamically imported module'
  • Safari
    • 'Importing a module script failed'

これを踏まえて、ハンドリングするときのコードは次の通りである。

if (error instanceof TypeError) {
  const CHUNK_LOAD_ERROR_MESSAGES = [
    'Failed to fetch dynamically imported module',
    'error loading dynamically imported module',
    'Importing a module script failed',
  ];

  if (CHUNK_LOAD_ERROR_MESSAGES.some((message) => error.message.includes(message))) {
    window.alert('アプリケーションが更新されました。ページをリロードしてください。');
    return;
  }
}

おわりに

上記の対応をおこなったことで ChunkLoadError が発生してもユーザーにフィードバックできるようになりました。また、エラートラッカーに投げる必要もなくなったため、アラートの S/N 比も改善しました。 大きな機能実装も大切ですが、ユーザーにも開発者にもいい体験を提供し続けるには日々の小さな改善を積み重ねることも、また大事だと感じます。

エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、Angular を使ったアプリケーション開発をしたい方、ぜひ採用ページからご応募をお願いいたします。

hrmos.co