このドキュメントは、Next.js v16の主要機能とベストプラクティスをまとめたものです。
目次
主要な変更点
Next.js 16で安定版となった機能
- Turbopack: デフォルトのバンドラーとして採用
- Cache Components: Partial Prerendering (PPR) の実装
- 非同期Request API: 完全に非同期化(後方互換性なし)
バージョンアップ時の注意点
# 公式codemodを使用した自動アップグレード(推奨)
npx @next/codemod@canary upgrade latest
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
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>
)
}
ランタイムAPI一覧:
cookies()headers()searchParamspropparamsprop(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>
)
}
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
}
Suspense境界がない場合のエラー
Cache Componentsを有効にすると、動的コードは必ずSuspenseでラップする必要があります。忘れると以下のエラーが表示されます:
Uncached data was accessed outside of <Suspense>
修正方法:
- コンポーネントを
<Suspense>でラップする - または
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')
対象API:
cookies()headers()draftMode()paramspropsearchParamsprop
移行方法
サーバーコンポーネント
// 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>
}
クライアントコンポーネント
'use client'
import { use } from 'react'
export default function Page({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = use(params)
return <div>{id}</div>
}
自動移行codemod
npx @next/codemod@canary next-async-request-api .
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を使用する場合}
}
ファイルシステムキャッシュ(ベータ版)
ビルド間でキャッシュを保存し、次回のビルドを高速化します。
// next.config.tsconst nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCache: true,
},
}
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')
}
// 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>
)
}
セキュリティのベストプラクティス
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()
}
// プロフィール更新処理
}
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 })
}
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)
}
クライアントコンポーネントでの使用
'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>
)
}
キャッシング戦略
キャッシュの種類
Next.js v16には複数のキャッシュレイヤーがあります:
- Router Cache (クライアント)
- Full Route Cache (サーバー)
- Data Cache (サーバー)
- 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>
}
コンポーネントレベル
import { cacheLife } from 'next/cache'
async function ProductList() {
'use cache'
cacheLife('minutes')
const products = await db.query('SELECT * FROM products')
return <ul>{/* 製品一覧 */}</ul>
}
関数レベル
import { cacheLife } from 'next/cache'
export async function getPosts() {
'use cache'
cacheLife('hours')
return await db.query('SELECT * FROM posts ORDER BY id DESC')
}
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時間
},
},
}
import { cacheLife } from 'next/cache'
export async function getBlogPosts() {
'use cache'
cacheLife('blog')
return await db.query('SELECT * FROM posts')
}
キャッシュの再検証
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')// 即座にキャッシュを無効化
}
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')// バックグラウンドで再検証
}
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}`)// 詳細ページ
}
再検証の使い分け
パフォーマンス最適化
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')
}
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 />
</>
)
}
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>
}
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>
)
}
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')
}
ルートセグメント設定の移行
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>
}
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>
}
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>
}
fetchCache → 不要
// Beforeexport const fetchCache = 'force-cache'
// After - use cacheを使用export default async function Page() {
'use cache'
// すべてのfetchが自動的にキャッシュされるreturn <div>...</div>
}
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')// 即座に無効化
}
Q4: Server Actionsでのcookie操作後、UIが更新されない
Server Actionでcookieを設定・削除した後、Next.jsは現在のページとレイアウトを再レンダリングします。キャッシュされたデータも更新したい場合は、revalidatePathやrevalidateTagを呼び出してください。
'use server'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
export async function updateTheme(theme: string) {
(await cookies()).set('theme', theme)
revalidatePath('/')// キャッシュも更新
}
チェックリスト
Next.js v16プロジェクトで確認すべき項目:
cacheComponents: trueを有効化- すべての動的コンポーネントに
Suspense境界を設置 - ランタイムAPIの呼び出しを
awaitに変更 use cacheでキャッシュ戦略を定義cacheLifeプロファイルを設定cacheTagでキャッシュにタグ付け- Server Actionsにセキュリティチェックを実装
- Turbopackの動作を確認
- 古いルートセグメント設定を削除
- パフォーマンス測定とキャッシュヒット率の確認
参考リンク
最終更新日: 2025-01-05 Next.js バージョン: 16.0.1