Documentation Next.js

はじめに

新しいWebアプリケーションを開発する際、フレームワークの選定はプロジェクトの成否を左右する重要な決定です。特にReactエコシステムでは、純粋なReact、Next.js、Gatsby、Remixなど複数の選択肢があり、どれを選ぶべきか迷うことも多いでしょう。

この記事では、Next.jsを技術選定する際の判断基準を、パフォーマンスSEO開発者体験(DX)スケーラビリティの4つの観点から詳しく解説します。実際のコード例を交えながら、Next.jsが適しているプロジェクトの特徴を明らかにします。

Next.jsとは

Next.jsは、Vercel社が開発したReactベースのフルスタックフレームワークです。React単体では実現が難しい**サーバーサイドレンダリング(SSR)静的サイト生成(SSG)**を標準でサポートしています。

主な特徴

特徴説明
ファイルベースルーティングファイル構造がそのままURLになる直感的なルーティング
複数のレンダリング戦略SSR、SSG、ISR、CSRを柔軟に選択可能
API Routesフロントエンドと同じプロジェクトでAPIを構築
画像最適化next/imageによる自動的な画像最適化
ゼロコンフィグ複雑な設定なしで開発を開始可能

パフォーマンス

Next.jsはパフォーマンス最適化を重視して設計されており、高速なWebアプリケーションを構築するための機能が豊富です。

レンダリング戦略の選択

Next.jsでは、ページごとに最適なレンダリング戦略を選択できます。

SSG(静的サイト生成)

ビルド時にHTMLを生成する方式です。CDNからの配信が可能で、最も高速です。

// app/blog/[slug]/page.tsx
// SSGを使用したブログ記事ページの例

// ビルド時に生成するパスを指定
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// ページコンポーネント
export default async function BlogPost({
  params,
}: {
  params: { slug: string };
}) {
  // ビルド時にデータを取得
  const post = await fetchPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

SSR(サーバーサイドレンダリング)

リクエストごとにサーバーでHTMLを生成する方式です。常に最新のデータを表示できます。

// app/dashboard/page.tsx
// SSRを使用したダッシュボードページの例

// キャッシュを無効化してSSRを強制
export const dynamic = "force-dynamic";

export default async function Dashboard() {
  // リクエストごとに最新データを取得
  const user = await getCurrentUser();
  const stats = await fetchUserStats(user.id);

  return (
    <div>
      <h1>ようこそ、{user.name}さん</h1>
      <DashboardStats stats={stats} />
    </div>
  );
}

ISR(増分静的再生成)

SSGとSSRの中間的なアプローチです。静的ページを定期的に再生成することで、パフォーマンスと鮮度を両立します。

// app/products/[id]/page.tsx
// ISRを使用した商品詳細ページの例

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  // 60秒間キャッシュし、その後バックグラウンドで再生成
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 60 },
  }).then((res) => res.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <p>価格: ¥{product.price.toLocaleString()}</p>
      <p>在庫: {product.stock}個</p>
    </div>
  );
}

自動的な最適化機能

Next.jsには、パフォーマンスを向上させる自動最適化機能が組み込まれています。

画像の最適化

next/imageコンポーネントを使用すると、画像が自動的に最適化されます。

import Image from "next/image";

export default function ProductCard({ product }) {
  return (
    <div className="product-card">
      {/* 自動的にWebP形式に変換、遅延読み込み、サイズ最適化 */}
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={300}
        height={200}
        // 画面に入る前に読み込む優先度を設定
        priority={false}
        // プレースホルダーでブラーを表示
        placeholder="blur"
        blurDataURL={product.blurDataUrl}
      />
      <h3>{product.name}</h3>
    </div>
  );
}

コードスプリッティング

Next.jsは自動的にページ単位でコードを分割し、必要なコードのみを読み込みます。

// 動的インポートによる手動のコードスプリッティング
import dynamic from "next/dynamic";

// 重いコンポーネントを遅延読み込み
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
  loading: () => <p>グラフを読み込み中...</p>,
  // SSRを無効化(クライアントサイドのみで使用するライブラリの場合)
  ssr: false,
});

export default function AnalyticsPage() {
  return (
    <div>
      <h1>分析ダッシュボード</h1>
      <HeavyChart />
    </div>
  );
}

SEO(検索エンジン最適化)

Next.jsはSEOに優れた設計がされており、検索エンジンのクローラーが適切にコンテンツをインデックス化できます。

メタデータの設定

Next.js 13以降では、metadataオブジェクトまたはgenerateMetadata関数でメタデータを設定します。

// app/blog/[slug]/page.tsx
import { Metadata } from "next";

// 動的なメタデータ生成
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await fetchPost(params.slug);

  return {
    title: `${post.title} | My Blog`,
    description: post.excerpt,
    // Open Graph(SNSシェア用)
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      publishedTime: post.publishedAt,
      authors: [post.author.name],
      images: [
        {
          url: post.ogImageUrl,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    // Twitter Card
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
      images: [post.ogImageUrl],
    },
  };
}

構造化データの追加

検索結果でリッチスニペットを表示するために、JSON-LDを追加できます。

// app/blog/[slug]/page.tsx
export default async function BlogPost({
  params,
}: {
  params: { slug: string };
}) {
  const post = await fetchPost(params.slug);

  // JSON-LD構造化データ
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    description: post.excerpt,
    author: {
      "@type": "Person",
      name: post.author.name,
    },
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    image: post.ogImageUrl,
  };

  return (
    <>
      {/* 構造化データをscriptタグで挿入 */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  );
}

サイトマップの自動生成

Next.js 13以降では、sitemap.tsファイルでサイトマップを動的に生成できます。

// app/sitemap.ts
import { MetadataRoute } from "next";

export default async function sitemap(): MetadataRoute.Sitemap {
  // 静的ページ
  const staticPages = [
    {
      url: "https://example.com",
      lastModified: new Date(),
      changeFrequency: "daily" as const,
      priority: 1,
    },
    {
      url: "https://example.com/about",
      lastModified: new Date(),
      changeFrequency: "monthly" as const,
      priority: 0.8,
    },
  ];

  // 動的ページ(ブログ記事など)
  const posts = await fetchAllPosts();
  const postPages = posts.map((post) => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: "weekly" as const,
    priority: 0.6,
  }));

  return [...staticPages, ...postPages];
}

開発者体験(DX)

Next.jsはReact開発者にとって馴染みやすく、生産性を高める機能が充実しています。

ファイルベースルーティング

ファイル構造がそのままURLに対応するため、直感的にルーティングを管理できます。

app/
├── page.tsx           # /
├── about/
│   └── page.tsx       # /about
├── blog/
│   ├── page.tsx       # /blog
│   └── [slug]/
│       └── page.tsx   # /blog/:slug
├── products/
│   ├── page.tsx       # /products
│   └── [...category]/
│       └── page.tsx   # /products/electronics/phones など
└── api/
    └── users/
        └── route.ts   # /api/users

API Routes

フロントエンドと同じプロジェクトでAPIエンドポイントを作成できます。

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

// GET /api/users
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get("page") || "1");
  const limit = parseInt(searchParams.get("limit") || "10");

  const users = await prisma.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({
    users,
    page,
    limit,
  });
}

// POST /api/users
export async function POST(request: NextRequest) {
  const body = await request.json();

  // バリデーション
  if (!body.email || !body.name) {
    return NextResponse.json(
      { error: "名前とメールアドレスは必須です" },
      { status: 400 }
    );
  }

  const user = await prisma.user.create({
    data: {
      email: body.email,
      name: body.name,
    },
  });

  return NextResponse.json(user, { status: 201 });
}

Server ActionsによるフォームM処理

Server Actionsを使用すると、フォーム処理をサーバーサイドで直接実行できます。

// app/contact/page.tsx
import { redirect } from "next/navigation";

// Server Action
async function submitContact(formData: FormData) {
  "use server";

  const name = formData.get("name") as string;
  const email = formData.get("email") as string;
  const message = formData.get("message") as string;

  // データベースに保存
  await prisma.contact.create({
    data: { name, email, message },
  });

  // メール送信
  await sendEmail({
    to: "admin@example.com",
    subject: `お問い合わせ: ${name}様`,
    body: message,
  });

  // 完了ページにリダイレクト
  redirect("/contact/success");
}

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <div>
        <label htmlFor="name">お名前</label>
        <input type="text" id="name" name="name" required />
      </div>
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input type="email" id="email" name="email" required />
      </div>
      <div>
        <label htmlFor="message">メッセージ</label>
        <textarea id="message" name="message" required />
      </div>
      <button type="submit">送信</button>
    </form>
  );
}

スケーラビリティと拡張性

Next.jsは大規模なアプリケーションでも拡張性を確保できる設計がされています。

Server ComponentsとClient Componentsの分離

React Server Componentsにより、サーバーとクライアントの責務を明確に分離できます。

// app/products/page.tsx (Server Component)
// デフォルトでServer Component - データベースに直接アクセス可能
import { ProductList } from "./ProductList";
import { AddToCartButton } from "./AddToCartButton";

export default async function ProductsPage() {
  // サーバーサイドでデータを取得
  const products = await prisma.product.findMany({
    include: { category: true },
  });

  return (
    <div>
      <h1>商品一覧</h1>
      <ProductList products={products} />
    </div>
  );
}

// components/AddToCartButton.tsx (Client Component)
"use client"; // クライアントコンポーネントとして明示

import { useState } from "react";

export function AddToCartButton({ productId }: { productId: string }) {
  const [isLoading, setIsLoading] = useState(false);

  const handleClick = async () => {
    setIsLoading(true);
    await addToCart(productId);
    setIsLoading(false);
  };

  return (
    <button onClick={handleClick} disabled={isLoading}>
      {isLoading ? "追加中..." : "カートに追加"}
    </button>
  );
}

ミドルウェアによる共通処理

認証やリダイレクトなどの共通処理をミドルウェアで一元管理できます。

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // 認証が必要なパス
  const protectedPaths = ["/dashboard", "/settings", "/admin"];
  const isProtectedPath = protectedPaths.some((path) =>
    request.nextUrl.pathname.startsWith(path)
  );

  if (isProtectedPath) {
    // トークンの確認
    const token = request.cookies.get("auth-token");

    if (!token) {
      // ログインページにリダイレクト
      const loginUrl = new URL("/login", request.url);
      loginUrl.searchParams.set("redirect", request.nextUrl.pathname);
      return NextResponse.redirect(loginUrl);
    }
  }

  // 管理者専用パスのチェック
  if (request.nextUrl.pathname.startsWith("/admin")) {
    const userRole = request.cookies.get("user-role")?.value;

    if (userRole !== "admin") {
      return NextResponse.redirect(new URL("/unauthorized", request.url));
    }
  }

  return NextResponse.next();
}

// ミドルウェアを適用するパスを指定
export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*"],
};

Next.jsが適しているプロジェクト

以下のようなプロジェクトでは、Next.jsの採用を強く推奨します。

適しているケース

プロジェクトタイプ理由
コーポレートサイトSSGによる高速表示とSEO対策が重要
ECサイトISRによる商品ページの動的更新とパフォーマンス両立
ブログ・メディアサイトSEOとコンテンツの鮮度が重要
ダッシュボードSSRによるリアルタイムデータ表示
SaaSAPI Routesによるフルスタック開発

他のフレームワークと比較検討すべきケース

  • 単純なSPA: React単体やViteで十分な場合
  • 完全な静的サイト: Astroのほうが軽量な場合がある
  • 複雑なバックエンド: 専用のバックエンドフレームワークとの組み合わせを検討

まとめ

Next.jsは、パフォーマンス、SEO、開発者体験、スケーラビリティのすべてにおいて優れた選択肢です。技術選定の際は、以下のポイントを確認しましょう。

  1. パフォーマンス要件: SSG、SSR、ISRの中から最適な戦略を選択
  2. SEO要件: メタデータ、構造化データ、サイトマップの自動生成が必要か
  3. チームの経験: Reactの知識があれば学習コストは低い
  4. プロジェクト規模: 小規模から大規模まで対応可能

Next.jsは継続的にアップデートされており、React Server ComponentsやServer Actionsなど最新のReact機能をいち早く取り入れています。長期的なプロジェクトでも安心して採用できるフレームワークです。

参考文献

円