Nuxtアプリケーションの表示速度が遅い原因の多くは、JavaScriptバンドルの肥大化にあります。バンドルサイズが大きいとダウンロード・パース・実行に時間がかかり、Core Web VitalsのLCP(Largest Contentful Paint)やTTI(Time to Interactive)を悪化させます。
Nuxt 4.2で導入されたasync data handler extraction機能により、事前レンダリングサイトではJavaScriptバンドルサイズが最大39%削減されたという実測値が報告されています(出典: Nuxt Blog v4.2)。ただしアプリケーション固有の最適化は開発者が行う必要があります。
本記事では「分析 → 最適化 → 監視」の3ステップで、Nuxt 3/4環境でのバンドルサイズ削減を体系的に進める方法を整理します。
バンドルサイズ肥大化の主な原因
Nuxtプロジェクトでバンドルが膨らむ典型的なパターンは以下の通りです。
| 原因 | 具体例 | 影響度 |
|---|---|---|
| 大型ライブラリの全量インポート | import _ from 'lodash' で全関数を読み込み | 高 |
| 未使用プラグインのグローバル登録 | plugins/ に配置したまま使っていないファイル | 中〜高 |
| 重いUIフレームワークの一括読み込み | Vuetifyなどのコンポーネントライブラリを全量バンドル | 高 |
| 国際化ファイルの全言語同梱 | @nuxtjs/i18n で全ロケールをバンドルに含める | 中〜高 |
| クライアント不要なロジックの同梱 | Markdownパーサーなどサーバーで完結する処理をクライアントにも送信 | 中 |
| 画像アセットの未最適化 | 未圧縮のPNG/JPEGをそのままバンドル | 中 |
ステップ1: nuxi analyzeでバンドル構成を可視化する
最適化に着手する前に、現状のバンドル構成を正確に把握する必要があります。Nuxt 3/4には nuxi analyze コマンドが組み込まれており、追加パッケージなしでバンドルの可視化が可能です(出典: Nuxt公式)。
npx nuxi analyze
このコマンドを実行すると、内部的に vite-bundle-visualizer が動作し、ツリーマップ形式でチャンクごとのサイズを表示するHTMLレポートがブラウザで開きます。
主なオプション
# ブラウザを自動で開かずにレポートファイルだけ生成
npx nuxi analyze --no-serve
# ログレベルを変更
npx nuxi analyze --logLevel=verbose
nuxt.config.tsからの設定
nuxt.config.ts で常に分析を有効にする方法もあります。
// nuxt.config.ts
export default defineNuxtConfig({
build: {
analyze: {
template: 'treemap',
filename: '.nuxt/analyze/{name}.html',
},
},
})
rollup-plugin-visualizerで詳細な分析
gzipサイズやbrotliサイズも含めた詳細な分析が必要な場合は、rollup-plugin-visualizer をViteプラグインとして追加します。
npm install -D rollup-plugin-visualizer
// nuxt.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineNuxtConfig({
vite: {
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: '.nuxt/stats.html',
}),
],
},
})
ツリーマップで大きなブロックが表示された場合、そのモジュールがバンドル分割やLazy Loading、または軽量な代替ライブラリへの置き換え対象候補となります。
ステップ2: Nuxt 3/4の組み込み機能で最適化する
Lazyコンポーネントで初期バンドルを軽量化
Nuxt 3/4では、コンポーネント名に Lazy プレフィックスを付けるだけで動的インポート(Dynamic Import)に変換されます(出典: Nuxt公式)。追加設定は不要です。
<template>
<div>
<!-- 常に表示するコンポーネント -->
<Header />
<!-- ボタンクリック時にはじめてJSが読み込まれる -->
<LazyContactForm v-if="showForm" />
<button @click="showForm = true">お問い合わせ</button>
<!-- スクロールして見える範囲に入ったときに読み込み -->
<LazyReviewList hydrate-on-visible />
</div>
</template>
<script setup lang="ts">
const showForm = ref(false)
</script>
モーダル、ドロワー、タブの非表示パネル、ページ下部のセクションなど、初期表示に不要なコンポーネントはすべて Lazy プレフィックスの候補です。
遅延ハイドレーションでJS実行を最適化
Nuxt 4では Lazy プレフィックスと組み合わせて、ハイドレーションのタイミングを制御する戦略が利用できます。サーバーサイドレンダリング済みのHTMLを即座に表示しつつ、JavaScriptの実行は必要なタイミングまで遅延させます。
| 属性 | ハイドレーションのタイミング | 用途 |
|---|---|---|
hydrate-on-visible | ビューポートに入った時 | ページ下部の要素 |
hydrate-on-idle | ブラウザがアイドル状態の時 | 優先度の低いUI |
hydrate-on-interaction | クリック・ホバー等の操作時 | ドロップダウン、アコーディオン |
hydrate-on-media-query | メディアクエリ条件合致時 | モバイル専用コンポーネント |
hydrate-never | ハイドレーションしない | 完全に静的なコンテンツ |
<!-- フッターはスクロールするまでハイドレーション不要 -->
<LazyFooter hydrate-on-visible />
<!-- サイドバーはブラウザアイドル時にハイドレーション -->
<LazySidebar hydrate-on-idle />
<!-- FAQアコーディオンはクリック時にハイドレーション -->
<LazyFaqAccordion hydrate-on-interaction="click" />
Server Componentsでクライアントバンドルからゼロ除外
.server.vue サフィックスを持つコンポーネントは、サーバーでのみレンダリングされます。レンダリング済みHTMLだけがクライアントに送信され、JavaScriptは一切含まれません。
components/
HeavyMarkdown.server.vue # サーバーのみ(クライアントJS = 0)
Chart.client.vue # クライアントのみ
Button.vue # 通常(SSR + ハイドレーション)
有効化設定:
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
componentIslands: true,
},
})
Markdownパーサー、シンタックスハイライト、PDF生成、重い計算処理など、クライアントで再実行する必要がないロジックに最適です。
ただし Server Components にはいくつかの制約があります。
- アプリケーションの他の部分とコンテキストを共有できない(アイランド隔離)
- Props変更時にサーバーへ再レンダリングリクエストが発生する
- 1ページに大量配置するとサーバー負荷が高まる
インタラクティブな子コンポーネントをServer Component内に配置する場合は、selectiveClient オプションを使います。
export default defineNuxtConfig({
experimental: {
componentIslands: {
selectiveClient: true,
},
},
})
<!-- サーバーコンポーネント内にクライアントコンポーネントを部分的に埋め込む -->
<template>
<div>
<HeavyMarkdown :content="markdownText" />
<LikeButton nuxt-client :post-id="123" />
</div>
</template>
Tree Shakingを最大化する設定
Options APIの除去
プロジェクトがComposition APIのみで構成されている場合、Vue 3のコンパイル時フラグでOptions API関連コードをバンドルから除去できます。
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
define: {
__VUE_OPTIONS_API__: false,
__VUE_PROD_DEVTOOLS__: false,
},
},
})
Options APIに依存するサードパーティライブラリ(一部のバージョンのVuetifyなど)を使用している場合、この設定を有効にすると動作しなくなるため事前確認が必要です。
Composableの環境別Tree Shaking
サーバー専用・クライアント専用のcomposableを適切にtree-shakeする設定です。Nuxt 4ではクライアント専用コンポーネントのtree-shakeがデフォルトで有効化されています。
// nuxt.config.ts
export default defineNuxtConfig({
optimization: {
treeShake: {
composables: {
server: {
// サーバービルドから除外するcomposable
},
client: {
// クライアントビルドから除外するcomposable
},
},
},
},
})
gzip / brotli事前圧縮で転送サイズを削減
Nitro(Nuxtのサーバーエンジン)に compressPublicAssets オプションを設定すると、ビルド時にJS・CSSファイルをgzip / brotliで事前圧縮します。CDNを使わない環境でも転送サイズを大幅に削減できます。
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
compressPublicAssets: {
gzip: true,
brotli: true,
},
},
})
ペイロード抽出で静的生成サイトを最適化
nuxt generate で静的サイトを生成している場合、payloadExtraction を有効にするとページのデータペイロードを別ファイルに切り出せます。クライアントサイドナビゲーション時にページ全体のJSを実行し直す代わりに、軽量な payload.json だけを取得します。
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
payloadExtraction: true,
},
})
ステップ3: ライブラリのインポートを見直す
Named Importに切り替える
ライブラリ全体をインポートすると、使っていない関数もバンドルに含まれます。必要な関数だけを指定してインポートする(Named Import)ことで、Tree Shakingが効きやすくなります。
// NG: ライブラリ全体がバンドルされる
import _ from 'lodash'
_.debounce(fn, 300)
// OK: debounce関数のコードだけがバンドルされる
import { debounce } from 'lodash-es'
debounce(fn, 300)
lodashの場合、CommonJS版(lodash)はTree Shakingが効かないため、ESM版(lodash-es)に切り替えるか、lodash/debounce のようにパス指定でインポートする必要があります。
主要ライブラリのバンドルサイズを確認する
ライブラリを導入する前に、Bundlephobia でサイズを確認する習慣をつけると、意図しないバンドル肥大化を防げます。
Nuxtエコシステムでよく使われるライブラリのサイズ比較:
| ライブラリ | gzip後サイズ | 備考 |
|---|---|---|
| Pinia | 約1 kB | 公式推奨の状態管理。極めて軽量(出典: Pinia公式) |
VueUse (@vueuse/core) | 関数単位で67B〜3.2kB | Tree Shakable設計。使用関数のみバンドル(出典: VueUse) |
| @nuxtjs/i18n | 19.24 MB(10MB翻訳時) | 大規模翻訳でバンドル肥大化の傾向あり |
| nuxt-i18n-micro | 1.5 MB(同条件) | 軽量な代替。機能は限定的だがバンドル大幅削減(出典: Nuxt Modules) |
| Day.js | 約2 kB | Moment.js(約70kB gzip)の軽量代替 |
重いライブラリの代替候補
| 元のライブラリ | gzipサイズ | 代替案 | gzipサイズ |
|---|---|---|---|
| Moment.js | 約72 kB | Day.js | 約2 kB |
| lodash | 約71 kB | lodash-es(Named Import) | 関数単位で数百B |
| Chart.js | 約65 kB | unovis / lightweight-charts | 約20〜30 kB |
| highlight.js(全言語) | 約300 kB | Server Componentで実行 | クライアント0 kB |
ステップ4: チャンク分割とルーティング最適化
manualChunksでベンダーチャンクを制御する
Viteのビルド設定で、特定のライブラリを独立したチャンクに分割できます。ブラウザキャッシュの効率が上がり、アプリケーションコードの変更時にベンダーチャンクの再ダウンロードを回避できます。
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('vue') || id.includes('@vue')) {
return 'vue-vendor'
}
if (id.includes('pinia')) {
return 'pinia'
}
return 'vendor'
}
},
},
},
chunkSizeWarningLimit: 500, // 500KB超で警告
},
},
})
routeRulesでページ単位のレンダリング戦略を設定する
ページごとに最適なレンダリング戦略を選ぶことで、不要なSSR処理やクライアントバンドルを削減できます。
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // ビルド時に静的HTML生成
'/blog/**': { swr: 3600 }, // 1時間キャッシュのSWR
'/products/**': { isr: 3600 }, // ISR(増分静的再生成)
'/admin/**': { ssr: false }, // 管理画面はSPA(SSR不要)
'/landing': { experimentalNoScripts: true }, // JSなしの静的ページ
},
})
experimentalNoScripts を設定すると、そのルートではNuxtのスクリプトとリソースヒントが出力されず、完全な静的HTMLとして配信されます。ランディングページや利用規約ページなどインタラクションが不要なページに有効です。
ステップ5: 画像を最適化する
画像はページ容量の大部分を占めることがあり、@nuxt/image モジュールを使うとビルド時の自動最適化が可能です(出典: Nuxt公式)。
npx nuxi module add @nuxt/image
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/image'],
image: {
quality: 80,
format: ['webp', 'avif'],
},
})
<template>
<!-- 自動で width/height 属性が付与され CLS を防止 -->
<NuxtImg
src="/hero.jpg"
width="1200"
height="630"
loading="eager"
fetchpriority="high"
format="webp"
/>
<!-- ファーストビュー外の画像は遅延読み込み -->
<NuxtImg
src="/feature.png"
width="600"
height="400"
loading="lazy"
/>
</template>
WebPはJPEGと比べて25〜34%程度ファイルサイズが小さく、AVIFはさらに20%程度削減できます(出典: Google Developers - WebP)。
ステップ6: CI/CDでバンドルサイズを継続監視する
最適化は一度やって終わりではありません。新しい依存関係の追加やコード変更によって、気づかないうちにバンドルサイズが増加することがあります。CI/CDパイプラインに監視を組み込むことで、PRごとにサイズ変化を検知できます。
nuxt-bundle-analysisを使ったPRコメント通知
nuxt-bundle-analysis はNuxt専用のツールで、PRにバンドルサイズの差分をコメントとして自動投稿します。
npx -p nuxt-bundle-analysis generate
このコマンドで .github/workflows/nuxt_bundle_analysis.yml が自動生成されます。
// package.json に追加
{
"nuxtBundleAnalysis": {
"statsFile": ".nuxt/stats/client.json",
"buildOutputDirectory": ".nuxt",
"minimumChangeThreshold": 0,
"builder": "vite"
}
}
size-limitでサイズ上限を設定する
size-limit はJSアプリのバンドルサイズに上限を設定し、超過時にCIを失敗させるツールです。ブラウザでの実行コスト(ダウンロード + パース + 実行時間)を算出できる点が特徴です。
npm install -D size-limit @size-limit/file
// package.json
{
"size-limit": [
{
"path": ".output/public/_nuxt/*.js",
"limit": "300 kB"
}
],
"scripts": {
"size": "size-limit",
"size:check": "size-limit --limit"
}
}
GitHub Actionsとの連携:
# .github/workflows/size-limit.yml
name: Bundle Size Check
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
最適化チェックリスト
実装の優先度順に整理します。
| 優先度 | 施策 | 期待効果 | 設定の複雑さ |
|---|---|---|---|
| 1 | nuxi analyze でボトルネック特定 | 最適化対象の明確化 | 低 |
| 2 | 未使用ライブラリ・プラグインの削除 | 数十〜数百kB削減 | 低 |
| 3 | Named Import / ESM版ライブラリへ切替 | 数十kB削減 | 低 |
| 4 | Lazy プレフィックスで初期バンドル削減 | 初期ロードの軽量化 | 低 |
| 5 | .server.vue で重い処理をサーバー専用に | クライアントJSゼロ化 | 中 |
| 6 | compressPublicAssets でgzip/brotli圧縮 | 転送サイズ60〜80%削減 | 低 |
| 7 | __VUE_OPTIONS_API__: false | Vue本体の軽量化 | 低(要互換確認) |
| 8 | 遅延ハイドレーション戦略の適用 | TTI改善 | 中 |
| 9 | manualChunks でキャッシュ効率向上 | 再訪問時のロード高速化 | 中 |
| 10 | @nuxt/image で画像最適化 | 画像転送サイズ25〜50%削減 | 低 |
| 11 | CI/CDでサイズ監視 | 回帰防止 | 中 |
今後の展望: RolldownとVapor Mode
Nuxtのバンドル最適化は今後さらに進化する見通しです。
Rolldown(Vite 8): Rustベースの高速バンドラーRolldownを搭載したVite 8のベータ版が2025年12月にリリースされました(出典: VoidZero)。Rolldownが安定版としてViteに統合されれば、Nuxtのビルドパフォーマンスもさらに向上します。現時点では rolldown-vite パッケージとして試験的に利用可能です。
Vapor Mode(Vue 3.6): Virtual DOMを使わない代替レンダリングモードで、SolidやSvelte 5と同等のパフォーマンスをベンチマークで達成しています(出典: Vue School)。Vue 3.6ベータで機能セットが完了しており、Composition API + <script setup> のみサポートします。安定版リリースは2026年が見込まれており、Nuxtへの対応も順次進む予定です。
まとめ
Nuxtアプリケーションのバンドルサイズ削減は、特別な技術知識がなくても nuxi analyze での現状把握から始められます。Lazyコンポーネント、Server Components、Tree Shakingの設定はいずれも nuxt.config.ts への数行の追記で実現でき、効果はすぐに計測可能です。
最も重要なのは、CI/CDパイプラインにバンドルサイズの監視を組み込み、一度達成した最適化を維持する仕組みを整えることです。nuxt-bundle-analysis や size-limit を導入すれば、PRごとにサイズ変化を自動検知でき、意図しないバンドル肥大化を防止できます。
