【Vue.js】コードスプリッティングと遅延読み込みで初期表示を高速化する方法

【Vue.js】コードスプリッティングと遅延読み込みで初期表示を高速化する方法 Vue.js

SPAは機能が増えるほど初期バンドルが肥大化し、ファーストペイントが遅くなりがちです。Vue.jsでは「コードスプリッティング(分割)」と「遅延読み込み(ダイナミックインポート)」を組み合わせ、必要な画面・コンポーネントだけを後から読み込むことで初期表示を高速化できます。この記事では、ルート単位・コンポーネント単位の分割から、UXを損なわない読み込み体験の作り方まで実装例で解説します。

ルート単位のコード分割(Vue Router × 動的インポート)

最も効果が高いのがページ(ルート)ごとの遅延読み込みです。import() を使うだけでビルド時に自動的にチャンクに分割されます。

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// ルートごとに遅延読み込み
const Home = () => import('@/pages/Home.vue')
const Dashboard = () => import('@/pages/Dashboard.vue')
const Settings = () => import('@/pages/Settings.vue')

export default createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/dashboard', component: Dashboard },
    { path: '/settings', component: Settings }
  ]
})

チャンク名を制御したい場合は、ビルドツールに応じてコメントを付与します(Viteは不要、Webpack系で有効)。

const Dashboard = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Dashboard.vue')

コンポーネント単位の遅延読み込み(defineAsyncComponent)

重いグラフやエディタなど、使用頻度が低いコンポーネントはページ内でも遅延読み込みが有効です。

// components/HeavyChart.async.js
import { defineAsyncComponent } from 'vue'

export const HeavyChart = defineAsyncComponent({
  loader: () => import('@/components/HeavyChart.vue'),
  // UX改善:ローディング・エラー・リトライ
  loadingComponent: () => import('@/components/LoadingSpinner.vue'),
  errorComponent: () => import('@/components/LoadError.vue'),
  delay: 200,        // ms: チラつき防止
  timeout: 30000     // ms: タイムアウト
})
<template>
  <HeavyChart />
</template>

<script setup>
import { HeavyChart } from './HeavyChart.async'
</script>

Suspenseで読み込み中のUIを制御(Composition API)

Vue 3では <Suspense> によって非同期コンポーネントのローディング状態を簡潔に表現できます。

<template>
  <Suspense>
    <template #default>
      <AsyncUserPanel />
    </template>
    <template #fallback>
      <SkeletonUserPanel />
    </template>
  </Suspense>
</template>

<script setup>
const AsyncUserPanel = defineAsyncComponent(() => import('@/components/UserPanel.vue'))
import SkeletonUserPanel from '@/components/skeletons/UserPanelSkeleton.vue'
</script>

上手なプリフェッチ/プリロードの使い分け

遅延読み込みと組み合わせると、近い将来必要になるチャンクはプリフェッチ、すぐ必要なものはプリロードが効果的です(ViteはHTMLの <link>、Webpack系はマジックコメント)。

// すぐ使う:preload(初期描画をブロックしうるため要計画)
const Hero = () => import(
  /* webpackPreload: true, webpackChunkName: "hero" */
  '@/components/Hero.vue'
)

// そのうち使う:prefetch(アイドル時にダウンロード)
const Help = () => import(
  /* webpackPrefetch: true, webpackChunkName: "help" */
  '@/pages/Help.vue'
)

条件付きで読み込む(機能フラグ・権限で分割)

ABテストやロール別機能は条件成立時のみ読み込み、初期コストを削減します。

async function mountAdmin() {
  if (currentUser.role === 'admin') {
    const { default: AdminPanel } = await import('@/features/admin/AdminPanel.vue')
    app.component('AdminPanel', AdminPanel)
  }
}

アセット(画像・翻訳辞書・データ)の遅延読み込み

コードだけでなく、「大量画像」「大きなi18n辞書」「初期に不要な定数データ」も分割対象です。

// i18n辞書を言語切替時にだけ取得
async function setLocale(lang) {
  const messages = await import(`./locales/${lang}.json`)
  i18n.global.setLocaleMessage(lang, messages.default)
  i18n.global.locale.value = lang
}

計測と検証:効果を数字で確認する

導入後は「初期バンドルサイズ」「TTFB/FP/FCP/LCP」を計測して効果を検証します。Viteなら rollup-plugin-visualizer、Webpackなら webpack-bundle-analyzer でチャンク構成を可視化できます。

# Vite
npm i -D rollup-plugin-visualizer
# vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default { plugins: [visualizer({ open: true })] }

実装時の落とし穴とベストプラクティス

  • 過剰分割は逆効果:チャンクが細かすぎるとHTTP要求が増え遅くなる。機能の固まりごとに分割。
  • ローディング体験を設計:スピナーよりもスケルトンUIが体感を改善。delayでチラつき回避。
  • キャッシュ戦略:ファイル名にハッシュを付与(Vite既定)。Cache-Control: immutable を活用。
  • 共通依存の重複:複数チャンクで同一ライブラリを重複バンドルしない設定(ViteのmanualChunks/最適化)。
  • エラーハンドリング:ネットワーク不良時のリトライUI・再読込ボタンを用意。

まとめ

Vueのコードスプリッティングと遅延読み込みは、初期表示の高速化に直結する定石です。ルート分割、非同期コンポーネント、Suspense、プリフェッチ/プリロードを適切に組み合わせ、測定ツールで効果を確認しながら最適化しましょう。UXを崩さないローディング設計とキャッシュ戦略まで含めて運用できれば、機能拡張とパフォーマンスを両立できます。