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.2kBTree Shakable設計。使用関数のみバンドル(出典: VueUse
@nuxtjs/i18n19.24 MB(10MB翻訳時)大規模翻訳でバンドル肥大化の傾向あり
nuxt-i18n-micro1.5 MB(同条件)軽量な代替。機能は限定的だがバンドル大幅削減(出典: Nuxt Modules
Day.js約2 kBMoment.js(約70kB gzip)の軽量代替

重いライブラリの代替候補

元のライブラリgzipサイズ代替案gzipサイズ
Moment.js約72 kBDay.js約2 kB
lodash約71 kBlodash-es(Named Import)関数単位で数百B
Chart.js約65 kBunovis / lightweight-charts約20〜30 kB
highlight.js(全言語)約300 kBServer 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 }}

最適化チェックリスト

実装の優先度順に整理します。

優先度施策期待効果設定の複雑さ
1nuxi analyze でボトルネック特定最適化対象の明確化
2未使用ライブラリ・プラグインの削除数十〜数百kB削減
3Named Import / ESM版ライブラリへ切替数十kB削減
4Lazy プレフィックスで初期バンドル削減初期ロードの軽量化
5.server.vue で重い処理をサーバー専用にクライアントJSゼロ化
6compressPublicAssets でgzip/brotli圧縮転送サイズ60〜80%削減
7__VUE_OPTIONS_API__: falseVue本体の軽量化低(要互換確認)
8遅延ハイドレーション戦略の適用TTI改善
9manualChunks でキャッシュ効率向上再訪問時のロード高速化
10@nuxt/image で画像最適化画像転送サイズ25〜50%削減
11CI/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-analysissize-limit を導入すれば、PRごとにサイズ変化を自動検知でき、意図しないバンドル肥大化を防止できます。