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を崩さないローディング設計とキャッシュ戦略まで含めて運用できれば、機能拡張とパフォーマンスを両立できます。