Skip to content

Vue/Vite アプリで発生する「TypeError: Failed to fetch dynamically imported module」の解決策

問題の概要

Vue 3とViteを使用したアプリケーションで、動的インポートを使用したルーティング設定がある場合、本番環境で以下のエラーが発生することがあります:

js
TypeError: Failed to fetch dynamically imported module

このエラーは主に新しいデプロイ後に発生し、クライアントが古いチャンクファイルを要求しようとする際に、そのファイルが既に存在しない場合に発生します。

根本原因

Viteはビルド時に動的インポートごとに個別のチャンクを生成し、デフォルトではコンテンツに基づいたハッシュ値をファイル名に付与します(例:Overview.abc123.js)。コンポーネントのコードが変更されるとハッシュ値も変更され、古いファイルは削除されます。

エラー発生の流れ:

  1. アプリケーションをデプロイ
  2. クライアントがサイトにアクセス
  3. コード変更後、再デプロイを実行
  4. チャンクファイルのハッシュ値が変更される(例:Overview.32ab1c.js
  5. クライアントが古いハッシュ値のファイル(Overview.abc123.js)を要求
  6. ファイルが存在しないためエラー発生

主要な解決策

1. ルーターエラーハンドリングの実装

最も効果的で一般的な解決策は、Vue Routerのエラーハンドラーを使用することです:

js
// router.js
export const router = createRouter({
  // ルーター設定
})

router.onError((error, to) => {
  if (
    error.message.includes('Failed to fetch dynamically imported module') ||
    error.message.includes('Importing a module script failed')
  ) {
    // 無限リダイレクトを防ぐためのチェック
    if (!to?.fullPath) {
      window.location.reload()
    } else {
      window.location = to.fullPath
    }
  }
})

この方法では、動的インポートの失敗を検出した場合にページを強制再読み込みし、最新のチャンクファイルを取得します。

TIP

Vite 4.4以上を使用している場合は、組み込みのvite:preloadErrorイベントを利用できます:

js
window.addEventListener('vite:preloadError', (event) => {
  window.location.reload()
})

2. リトライメカニズムの実装

より洗練された解決策として、リトライ回数を制限する方法もあります:

js
export const errorReload = (error, retries = 1) => {
  const url = new URL(window.location.href)
  const errRetries = parseInt(url.searchParams.get('errRetries') || '0')
  
  if (errRetries >= retries) {
    window.history.replaceState(null, '', url.pathname)
    return
  }

  url.searchParams.set('errRetries', String(errRetries + 1))
  window.location.href = url.toString()
}

// ルーターエラーハンドラー内で使用
router.onError((error) => {
  if (error.message.includes('Failed to fetch dynamically imported module')) {
    errorReload(`Error during page load: ${error.message}`, 3)
  }
})

3. キャッシュ戦略の最適化(Service Worker使用時)

Service Workerを使用する場合、適切なキャッシュ戦略を実装することで問題を軽減できます:

js
// ワークボックスを使用したキャッシュ戦略の例
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

registerRoute(
  /\.js$/,
  new StaleWhileRevalidate({
    cacheName: 'js-cache',
  })
)

このアプローチでは、キャッシュから即時にコンテンツを提供しつつ、バックグラウンドで最新のバージョンをチェックします。

その他の検討すべき解決策

開発環境での問題解決

開発サーバーでこの問題が発生する場合:

bash
# 開発サーバーを停止した後
rm -rf node_modules/.cache node_modules/.vite
# サーバー再起動
npm run dev

VuetifyなどのUIフレームワーク使用時の対処

Vuetifyを使用している場合、最適化から除外することで問題が解決することがあります:

js
// vite.config.js
export default defineConfig({
  optimizeDeps: {
    exclude: ['vuetify']
  }
})

静的インポートへの変更

アプリが小規模な場合は、動的インポートを静的インポートに変更することも選択肢です:

js
// 動的インポート
// component: () => import('@/views/Home.vue')

// 静的インポートに変更
import Home from '@/views/Home.vue'
// ...
component: Home

予防策とベストプラクティス

  1. ファイル拡張子の明示: Viteではインポート時にファイル拡張子を明示することが推奨されます

    js
    // ✅ 推奨
    import MyComponent from 'components/MyComponent.vue'
    // ❌ 非推奨(Webpackでは動作するがViteでは問題の原因になる)
    import MyComponent from 'components/MyComponent'
  2. 定期的なキャッシュクリア: デプロイプロセスにキャッシュクリア手順を組み込む

  3. 適切なファイル命名: 大文字小文字の誤りなど、ファイル名の不一致に注意する

まとめ

「Failed to fetch dynamically imported module」エラーは、Viteの動的インポートとデプロイプロセスに起因する一般的な問題です。ルーターのエラーハンドリングを適切に実装し、キャッシュ戦略を最適化することで、ユーザー体験を損なうことなく問題を解決できます。

WARNING

本番環境でのみ発生する問題のため、ローカル環境では再現が困難です。Sentryなどのエラー追跡ツールを導入して、実際のユーザーへの影響を監視することをお勧めします。