Documentation Next.js

はじめに

Next.jsでの静的サイト生成(SSG: Static Site Generation)は、高速なWebサイトを構築するために優れた選択肢です。しかし、ページ数が増えるにつれてビルド時間が長くなり、開発効率やデプロイ速度に影響を与えることがあります。

この記事では、SSGサイトのビルド時間を効率的に短縮するための実践的なテクニックを紹介します。ISR(Incremental Static Regeneration)の活用から、動的ルートの最適化、キャッシュ戦略まで、大規模プロジェクトでも効果的なビルド管理方法を解説します。

ビルド時間が長くなる主な原因

まず、SSGサイトのビルド時間が長くなる原因を理解しましょう。

原因説明
ページ数の増加全ページをビルド時に生成するため、ページ数に比例して時間が増加
データ取得の遅延外部APIやデータベースからのデータ取得に時間がかかる
画像最適化大量の画像の最適化処理が負荷となる
依存パッケージ不要なパッケージや重い依存関係がビルドを遅くする

Incremental Static Regeneration(ISR)の活用

ISR(Incremental Static Regeneration)は、Next.jsが提供する強力な機能で、全てのページをビルド時に生成するのではなく、特定のページを動的に再生成することができます。これにより、更新が必要なページだけが生成されるため、大規模なサイトでもビルド時間を大幅に削減できます。

ISRの基本的な使い方

getStaticPropsrevalidateオプションを設定することで、指定された時間(秒)ごとにページを再生成できます。

// pages/posts/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';

interface Post {
  slug: string;
  title: string;
  content: string;
  updatedAt: string;
}

interface PostPageProps {
  post: Post;
}

// 静的ページの生成
export const getStaticProps: GetStaticProps<PostPageProps> = async ({ params }) => {
  // 外部APIからデータを取得
  const res = await fetch(`https://api.example.com/posts/${params?.slug}`);
  const post: Post = await res.json();

  return {
    props: { post },
    // 60秒ごとにページを再生成(ISRの核心部分)
    revalidate: 60,
  };
};

export const getStaticPaths: GetStaticPaths = async () => {
  // 人気のある記事のみビルド時に生成
  const res = await fetch('https://api.example.com/posts/popular');
  const popularPosts: Post[] = await res.json();

  const paths = popularPosts.map((post) => ({
    params: { slug: post.slug },
  }));

  return {
    paths,
    // 他のページはリクエスト時に生成
    fallback: 'blocking',
  };
};

export default function PostPage({ post }: PostPageProps) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>最終更新: {post.updatedAt}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

ISRの動作フロー

ISRの動作は以下のように進みます。

  1. 初回リクエスト: キャッシュされたページを即座に返す
  2. revalidate時間経過後: バックグラウンドでページを再生成
  3. 再生成完了: 新しいページでキャッシュを更新
  4. 次回リクエスト: 更新されたページを返す

この仕組みにより、ユーザーは常に高速なレスポンスを得られ、コンテンツも最新の状態に保たれます。

動的ルートとfallbackの使用

動的ルートを利用する場合、getStaticPathsで全てのルートを事前に生成することは避け、fallbackオプションを活用することで、ビルド時の負担を軽減できます。

fallbackオプションの比較

オプション動作使用場面
false未生成のパスは404を返すページ数が少なく、全て事前に把握できる場合
trueローディング状態を表示後、生成UXを重視し、ローディングUIを表示したい場合
'blocking'生成完了まで待機してから表示SEOを重視し、完全なHTMLを返したい場合

実践的なfallback: 'blocking'の実装

// pages/products/[category]/[id].tsx
import { GetStaticProps, GetStaticPaths } from 'next';

interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  description: string;
}

interface ProductPageProps {
  product: Product;
}

export const getStaticPaths: GetStaticPaths = async () => {
  // ビルド時には人気カテゴリの上位商品のみ生成
  const topProducts = await fetch('https://api.example.com/products/top-100');
  const products: Product[] = await topProducts.json();

  const paths = products.map((product) => ({
    params: {
      category: product.category,
      id: product.id,
    },
  }));

  return {
    paths,
    // 未生成のページはリクエスト時に動的に生成
    fallback: 'blocking',
  };
};

export const getStaticProps: GetStaticProps<ProductPageProps> = async ({ params }) => {
  try {
    const res = await fetch(
      `https://api.example.com/products/${params?.category}/${params?.id}`
    );

    // 商品が見つからない場合は404を返す
    if (!res.ok) {
      return { notFound: true };
    }

    const product: Product = await res.json();

    return {
      props: { product },
      // 5分ごとに再生成
      revalidate: 300,
    };
  } catch (error) {
    return { notFound: true };
  }
};

export default function ProductPage({ product }: ProductPageProps) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>カテゴリ: {product.category}</p>
      <p>価格: {product.price.toLocaleString()}</p>
      <p>{product.description}</p>
    </div>
  );
}

キャッシュの活用と再生成の最適化

ビルド時間を短縮するもう一つの重要な方法は、キャッシュを適切に利用することです。

ページごとのrevalidate設定

ページの更新頻度に応じて、適切な再生成間隔を設定しましょう。

// lib/revalidate-config.ts

// ページタイプごとの再生成間隔(秒)
export const REVALIDATE_INTERVALS = {
  // ニュース記事: 5分ごと(頻繁に更新)
  news: 60 * 5,
  // ブログ記事: 1時間ごと(中程度の更新頻度)
  blog: 60 * 60,
  // 会社情報: 1日ごと(ほとんど変更なし)
  company: 60 * 60 * 24,
  // 静的ページ: 1週間ごと
  static: 60 * 60 * 24 * 7,
} as const;

// 使用例
// pages/news/[id].tsx
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const article = await fetchNewsArticle(params?.id);

  return {
    props: { article },
    revalidate: REVALIDATE_INTERVALS.news,
  };
};

On-Demand ISR(オンデマンド再生成)

Next.js 12.1以降では、APIルートからオンデマンドでページを再生成できます。これにより、コンテンツ更新時に即座にページを更新できます。

// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';

// 再生成用のシークレットトークン(環境変数で管理)
const REVALIDATE_SECRET = process.env.REVALIDATE_SECRET;

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // シークレットトークンの検証
  if (req.query.secret !== REVALIDATE_SECRET) {
    return res.status(401).json({ message: '無効なトークンです' });
  }

  // 再生成するパスを取得
  const path = req.query.path as string;

  if (!path) {
    return res.status(400).json({ message: 'パスが指定されていません' });
  }

  try {
    // 指定されたパスを再生成
    await res.revalidate(path);
    return res.json({
      revalidated: true,
      message: `${path}を再生成しました`,
    });
  } catch (err) {
    // 再生成に失敗した場合
    return res.status(500).json({
      message: '再生成に失敗しました',
    });
  }
}

CMSのWebhookと連携することで、コンテンツ更新時に自動で再生成できます。

// pages/api/cms-webhook.ts
import type { NextApiRequest, NextApiResponse } from 'next';

interface WebhookPayload {
  event: string;
  slug: string;
  contentType: string;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // POSTリクエストのみ受け付け
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  const payload: WebhookPayload = req.body;

  // コンテンツタイプに応じてパスを決定
  const pathMap: Record<string, string> = {
    post: `/posts/${payload.slug}`,
    product: `/products/${payload.slug}`,
    page: `/${payload.slug}`,
  };

  const path = pathMap[payload.contentType];

  if (path) {
    try {
      await res.revalidate(path);
      // トップページも再生成(一覧に影響がある場合)
      await res.revalidate('/');
      return res.json({ revalidated: true });
    } catch (error) {
      return res.status(500).json({ message: '再生成に失敗しました' });
    }
  }

  return res.status(400).json({ message: '不明なコンテンツタイプです' });
}

並列処理の制御

ビルド時間を短縮するもう一つの方法として、並列処理の制御があります。大量のページを一度にビルドしようとすると、サーバーリソースを消費しすぎてしまうことがあります。

next.config.jsでの設定

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 実験的な機能を有効化
  experimental: {
    // ビルド時のワーカー数を制限(メモリ使用量を抑制)
    workerThreads: true,
    cpus: 4, // 使用するCPUコア数を制限
  },

  // 画像最適化の設定
  images: {
    // ビルド時の画像最適化を制限
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30日間キャッシュ
    // 外部画像のドメインを指定
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
      },
    ],
  },

  // webpack設定のカスタマイズ
  webpack: (config, { isServer }) => {
    // サーバーサイドビルドの最適化
    if (isServer) {
      config.optimization = {
        ...config.optimization,
        // コード分割の最適化
        splitChunks: {
          chunks: 'all',
          minSize: 20000,
          maxSize: 244000,
        },
      };
    }
    return config;
  },
};

module.exports = nextConfig;

環境変数でのビルド制御

# .env.production

# ビルド時のNode.jsメモリ制限を増加
NODE_OPTIONS=--max-old-space-size=4096

# 並列処理数の制限
NEXT_BUILD_CONCURRENCY=5

その他のビルド時間短縮テクニック

1. 依存パッケージの最適化

# 不要なパッケージを削除
npm prune --production

# パッケージのサイズを確認
npx depcheck

2. 動的インポートの活用

// 重いコンポーネントを動的にインポート
import dynamic from 'next/dynamic';

// チャートライブラリなど、重いコンポーネントは動的インポート
const HeavyChart = dynamic(() => import('../components/HeavyChart'), {
  loading: () => <p>読み込み中...</p>,
  ssr: false, // サーバーサイドレンダリングを無効化
});

3. ビルドキャッシュの活用(CI/CD環境)

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      # Next.jsのビルドキャッシュを保存
      - name: Cache Next.js build
        uses: actions/cache@v4
        with:
          path: |
            .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

まとめ

Next.jsのSSGビルド時間を短縮するには、以下のテクニックを組み合わせることが効果的です。

テクニック効果適用場面
ISRの活用ビルド時の生成ページ数を削減更新頻度の高いページ
fallback: ‘blocking’初回ビルドの負荷を軽減大量の動的ページ
On-Demand ISR必要なときだけ再生成CMS連携サイト
並列処理の制御リソース使用量を最適化大規模プロジェクト
ビルドキャッシュ増分ビルドの高速化CI/CD環境

これらのテクニックを適切に組み合わせることで、ビルド時間を大幅に短縮し、効率的なサイト管理を実現できます。

参考文献

円