Next.js2025/11/524分で読める
🙌

Next.js v16のベストプラクティス集

このドキュメントは、Next.js v16の主要機能とベストプラクティスをまとめたものです。

S

ShinCode

AI開発エンジニア

フォロワー 12.5K

Next.js v16のベストプラクティス集

このドキュメントは、Next.js v16の主要機能とベストプラクティスをまとめたものです。

目次

  1. 主要な変更点
  2. Cache Components (PPR)
  3. 非同期Request API
  4. Turbopack
  5. Server Actions
  6. キャッシング戦略
  7. パフォーマンス最適化

主要な変更点

Next.js 16で安定版となった機能

  • Turbopack: デフォルトのバンドラーとして採用
  • Cache Components: Partial Prerendering (PPR) の実装
  • 非同期Request API: 完全に非同期化(後方互換性なし)

バージョンアップ時の注意点

# 公式codemodを使用した自動アップグレード(推奨)
npx @next/codemod@canary upgrade latest
bash

Cache Components (PPR)

概要

Cache Componentsは、Next.js v16の最重要機能で、動的by defaultの新しいレンダリングモデルです。

従来の問題点

  • 全ページが静的 → パーソナライズできない
  • 全ページが動的 → 初期読み込みが遅い

Cache Componentsの解決策

  • 動的がデフォルト: すべてのルートは動的に動作
  • 細かい制御: コンポーネント・関数レベルでキャッシュを制御
  • PPRの実装: 静的シェル + 動的ストリーミング

有効化方法

// next.config.tsimport type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig
typescript

3つの重要なツール

1. Suspenseでランタイムデータを扱う

ランタイムAPIを使用するコンポーネントはSuspenseでラップする必要があります。

import { Suspense } from 'react'
import { cookies } from 'next/headers'

async function User() {
  const session = (await cookies()).get('session')?.value
  return <div>ユーザー: {session}</div>
}

export default function Page() {
  return (
    <section>
      <h1>これはプリレンダリングされます</h1>
      <Suspense fallback={<div>読み込み中...</div>}>
        <User />
      </Suspense>
    </section>
  )
}
typescript

ランタイムAPI一覧:

  • cookies()
  • headers()
  • searchParams prop
  • params prop(generateStaticParamsなしの場合)

2. Suspenseで動的データを扱う

データベースクエリやfetchリクエストなど、変化するデータもSuspenseでラップします。

import { Suspense } from 'react'

async function Posts() {
  const posts = await db.query('SELECT * FROM posts')
  return <div>{/* 投稿一覧 */}</div>
}

export default function Page() {
  return (
    <Suspense fallback={<PostsSkeleton />}>
      <Posts />
    </Suspense>
  )
}
typescript

3. use cacheでキャッシュ可能なデータを定義

import { cacheLife } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheLife('hours')

  const products = await db.query('SELECT * FROM products')
  return products
}
typescript

Suspense境界がない場合のエラー

Cache Componentsを有効にすると、動的コードは必ずSuspenseでラップする必要があります。忘れると以下のエラーが表示されます:

Uncached data was accessed outside of <Suspense>

修正方法:

  1. コンポーネントを<Suspense>でラップする
  2. またはuse cacheを使ってキャッシュ可能にする

非同期Request API

変更内容

Next.js 15で導入され、v16で完全に非同期化されました(同期アクセスは削除)。

対象API

// ❌ v15以前(同期)const cookieStore = cookies()
const session = cookieStore.get('session')

// ✅ v16(非同期)const cookieStore = await cookies()
const session = cookieStore.get('session')
typescript

対象API:

  • cookies()
  • headers()
  • draftMode()
  • params prop
  • searchParams prop

移行方法

サーバーコンポーネント

// Beforeexport default function Page({ params }: { params: { id: string } }) {
  const { id } = params
  return <div>{id}</div>
}

// Afterexport default async function Page({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <div>{id}</div>
}
typescript

クライアントコンポーネント

'use client'
import { use } from 'react'

export default function Page({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = use(params)
  return <div>{id}</div>
}
typescript

自動移行codemod

npx @next/codemod@canary next-async-request-api .
bash

Turbopack

概要

Next.js 16からデフォルトのバンドラーとして採用されました。

主な利点

  • 開発サーバーの高速起動
  • Fast Refresh(HMR)の大幅な高速化
  • ビルド時間の短縮

使用方法

// package.json{
  "scripts": {
    "dev": "next dev",// Turbopackを使用(デフォルト)"build": "next build",// Turbopackを使用(デフォルト)"dev:webpack": "next dev --webpack",// Webpackを使用する場合"build:webpack": "next build --webpack"// Webpackを使用する場合}
}
json

ファイルシステムキャッシュ(ベータ版)

ビルド間でキャッシュを保存し、次回のビルドを高速化します。

// next.config.tsconst nextConfig: NextConfig = {
  experimental: {
    turbopackFileSystemCache: true,
  },
}
typescript

Server Actions

基本的な使い方

// app/actions.ts'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const body = formData.get('body') as string

  await db.insert('posts', { title, body })

  revalidatePath('/posts')
  redirect('/posts')
}
typescript
// app/create/page.tsximport { createPost } from '../actions'

export default function CreatePage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">投稿</button>
    </form>
  )
}
typescript

セキュリティのベストプラクティス

1. 認証チェック

'use server'

import { cookies } from 'next/headers'
import { unauthorized } from 'next/navigation'

export async function updateProfile(formData: FormData) {
  const session = (await cookies()).get('session')?.value

  if (!session) {
    unauthorized()
  }

// プロフィール更新処理
}
typescript

2. 権限チェック

'use server'

import { forbidden } from 'next/navigation'

export async function deletePost(postId: string) {
  const user = await getCurrentUser()
  const post = await getPost(postId)

  if (post.userId !== user.id && user.role !== 'admin') {
    forbidden()
  }

  await db.delete('posts', { id: postId })
}
typescript

3. 入力検証

'use server'

import { z } from 'zod'

const PostSchema = z.object({
  title: z.string().min(1).max(100),
  body: z.string().min(1).max(5000),
})

export async function createPost(formData: FormData) {
  const parsed = PostSchema.parse({
    title: formData.get('title'),
    body: formData.get('body'),
  })

  await db.insert('posts', parsed)
}
typescript

クライアントコンポーネントでの使用

'use client'

import { useActionState } from 'react'
import { createPost } from './actions'

export default function CreateForm() {
  const [state, formAction, isPending] = useActionState(createPost, null)

  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit" disabled={isPending}>
        {isPending ? '送信中...' : '投稿'}
      </button>
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  )
}
typescript

キャッシング戦略

キャッシュの種類

Next.js v16には複数のキャッシュレイヤーがあります:

  1. Router Cache (クライアント)
  2. Full Route Cache (サーバー)
  3. Data Cache (サーバー)
  4. Cache Components (新機能)

use cacheの使い方

ページレベル

import { cacheLife } from 'next/cache'

export default async function Page() {
  'use cache'
  cacheLife('hours')

  const data = await fetchData()
  return <div>{data}</div>
}
typescript

コンポーネントレベル

import { cacheLife } from 'next/cache'

async function ProductList() {
  'use cache'
  cacheLife('minutes')

  const products = await db.query('SELECT * FROM products')
  return <ul>{/* 製品一覧 */}</ul>
}
typescript

関数レベル

import { cacheLife } from 'next/cache'

export async function getPosts() {
  'use cache'
  cacheLife('hours')

  return await db.query('SELECT * FROM posts ORDER BY id DESC')
}
typescript

cacheLifeプロファイル

// next.config.tsconst nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    blog: {
      stale: 60,// 1分間は古くても提供revalidate: 900,// 15分後に再検証expire: 86400,// 24時間後に期限切れ
    },
    product: {
      stale: 300,// 5分revalidate: 3600,// 1時間expire: 86400,// 24時間
    },
  },
}
typescript
import { cacheLife } from 'next/cache'

export async function getBlogPosts() {
  'use cache'
  cacheLife('blog')

  return await db.query('SELECT * FROM posts')
}
typescript

キャッシュの再検証

1. updateTag - 即座に更新

Server Action内で即座にキャッシュを無効化し、次のリクエストで新しいデータを取得します。

import { cacheTag, updateTag } from 'next/cache'

export async function getCart() {
  'use cache'
  cacheTag('cart')

  return await db.query('SELECT * FROM cart')
}

export async function addToCart(itemId: string) {
  'use server'

  await db.insert('cart', { itemId })
  updateTag('cart')// 即座にキャッシュを無効化
}
typescript

2. revalidateTag - stale-while-revalidate

古いデータを返しながら、バックグラウンドで再検証します。

import { cacheTag, revalidateTag } from 'next/cache'

export async function getPosts() {
  'use cache'
  cacheTag('posts')

  return await db.query('SELECT * FROM posts')
}

export async function createPost(formData: FormData) {
  'use server'

  await db.insert('posts', {/* ... */ })
  revalidateTag('posts', 'max')// バックグラウンドで再検証
}
typescript

3. revalidatePath - パス単位で再検証

'use server'

import { revalidatePath } from 'next/cache'

export async function updatePost(postId: string, data: any) {
  await db.update('posts', { id: postId }, data)

  revalidatePath('/posts')// 一覧ページrevalidatePath(`/posts/${postId}`)// 詳細ページ
}
typescript

再検証の使い分け

未対応のブロックタイプ: table

パフォーマンス最適化

1. 適切なキャッシュ戦略を選択

// ❌ 悪い例: すべてを動的にするexport default async function Page() {
  const posts = await db.query('SELECT * FROM posts')
  return <div>{/* ... */}</div>
}

// ✅ 良い例: 変更頻度の低いデータはキャッシュexport default async function Page() {
  const posts = await getCachedPosts()
  return <div>{/* ... */}</div>
}

async function getCachedPosts() {
  'use cache'
  cacheLife('hours')

  return await db.query('SELECT * FROM posts')
}
typescript

2. Suspense境界を適切に配置

// ❌ 悪い例: すべてを1つのSuspenseでラップexport default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <Header />      {/* 静的 */}
      <UserInfo />    {/* 動的 */}
      <Posts />       {/* 動的 */}
      <Footer />      {/* 静的 */}
    </Suspense>
  )
}

// ✅ 良い例: 動的部分のみをSuspenseでラップexport default function Page() {
  return (
    <>
      <Header />
      <Suspense fallback={<UserSkeleton />}>
        <UserInfo />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
      <Footer />
    </>
  )
}
typescript

3. 並列データフェッチ

// ❌ 悪い例: 逐次実行async function Page() {
  const user = await getUser()
  const posts = await getPosts()
  const comments = await getComments()

  return <div>{/* ... */}</div>
}

// ✅ 良い例: 並列実行async function Page() {
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
  ])

  return <div>{/* ... */}</div>
}
typescript

4. プリフェッチの活用

import Link from 'next/link'

export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          {/* Linkコンポーネントは自動的にプリフェッチ */}
          <Link href={`/posts/${post.id}`} prefetch={true}>
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}
typescript

5. Route Handlerのキャッシュ

// app/api/products/route.tsimport { cacheLife } from 'next/cache'

export async function GET() {
  const products = await getProducts()
  return Response.json(products)
}

async function getProducts() {
  'use cache'
  cacheLife('hours')

  return await db.query('SELECT * FROM products')
}
typescript

ルートセグメント設定の移行

Cache Componentsを有効にすると、従来のルートセグメント設定は不要または非サポートになります。

移行ガイド

dynamic = "force-dynamic" → 不要

// Beforeexport const dynamic = 'force-dynamic'

export default function Page() {
  return <div>...</div>
}

// After - 削除するだけ(デフォルトで動的)export default function Page() {
  return <div>...</div>
}
typescript

dynamic = "force-static" → use cache

// Beforeexport const dynamic = 'force-static'

export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}

// Afterexport default async function Page() {
  'use cache'
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}
typescript

revalidate → cacheLife

// Beforeexport const revalidate = 3600

export default async function Page() {
  return <div>...</div>
}

// Afterimport { cacheLife } from 'next/cache'

export default async function Page() {
  'use cache'
  cacheLife('hours')
  return <div>...</div>
}
typescript

fetchCache → 不要

// Beforeexport const fetchCache = 'force-cache'

// After - use cacheを使用export default async function Page() {
  'use cache'
// すべてのfetchが自動的にキャッシュされるreturn <div>...</div>
}
typescript

runtime = 'edge' → 非サポート

Cache ComponentsはNode.jsランタイムが必要です。Edge Runtimeは使用できません。


よくある質問

Q1: Cache ComponentsはPPRを置き換えますか?

いいえ。Cache Components は PPR を実装するための機能です。実験的なPPRフラグは削除されましたが、PPRそのものは継続されます。

Q2: 何をキャッシュすべきですか?

  • ランタイムデータに依存しないデータ
  • 変更頻度の低いデータ
  • 複数のリクエストで同じ値を返しても問題ないデータ

CMSなどでは、長めのキャッシュ期間を設定し、revalidateTagで更新通知を受け取る戦略が有効です。

Q3: キャッシュされたコンテンツを素早く更新するには?

cacheTagでデータにタグを付け、updateTagまたはrevalidateTagをトリガーします。

import { cacheTag, updateTag } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheTag('products')
  return await db.query('SELECT * FROM products')
}

export async function updateProduct(id: string, data: any) {
  'use server'
  await db.update('products', { id }, data)
  updateTag('products')// 即座に無効化
}
typescript

Q4: Server Actionsでのcookie操作後、UIが更新されない

Server Actionでcookieを設定・削除した後、Next.jsは現在のページとレイアウトを再レンダリングします。キャッシュされたデータも更新したい場合は、revalidatePathrevalidateTagを呼び出してください。

'use server'

import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'

export async function updateTheme(theme: string) {
  (await cookies()).set('theme', theme)
  revalidatePath('/')// キャッシュも更新
}
typescript

チェックリスト

Next.js v16プロジェクトで確認すべき項目:

  • cacheComponents: trueを有効化
  • すべての動的コンポーネントにSuspense境界を設置
  • ランタイムAPIの呼び出しをawaitに変更
  • use cacheでキャッシュ戦略を定義
  • cacheLifeプロファイルを設定
  • cacheTagでキャッシュにタグ付け
  • Server Actionsにセキュリティチェックを実装
  • Turbopackの動作を確認
  • 古いルートセグメント設定を削除
  • パフォーマンス測定とキャッシュヒット率の確認

参考リンク


最終更新日: 2025-01-05 Next.js バージョン: 16.0.1

タグ

著者について

S

ShinCode

AI開発エンジニア。生成AIを活用した開発手法の研究と実践に従事。 最新のAI技術を使った効率的な開発方法を日々探求しています。