11 KiB
Next.js 16 Migration Guide
From: Next.js 15.x To: Next.js 16.0.0 Last Updated: 2025-10-24
Table of Contents
- Overview
- Breaking Changes
- New Features
- Migration Steps
- Automated Migration
- Manual Migration
- Troubleshooting
Overview
Next.js 16 introduces significant changes:
- Breaking Changes: 6 major breaking changes
- New Features: Cache Components, updated caching APIs, React 19.2
- Performance: Turbopack stable, 2–5× faster builds
- Migration Time: ~1-2 hours for medium-sized apps
Recommendation: Use automated codemod first, then manually fix remaining issues.
Breaking Changes
1. Async Route Parameters ⚠️
What Changed: params, searchParams, cookies(), headers(), draftMode() are now async.
Before (Next.js 15):
export default function Page({ params, searchParams }) {
const slug = params.slug
const query = searchParams.q
}
After (Next.js 16):
export default async function Page({ params, searchParams }) {
const { slug } = await params
const { q: query } = await searchParams
}
TypeScript Types:
// Before
type PageProps = {
params: { slug: string }
searchParams: { q: string }
}
// After
type PageProps = {
params: Promise<{ slug: string }>
searchParams: Promise<{ q: string }>
}
Fix:
- Add
asyncto function - Add
awaitbefore params/searchParams - Update TypeScript types to
Promise<>
2. Middleware → Proxy ⚠️
What Changed: middleware.ts is deprecated. Use proxy.ts instead.
Migration:
# 1. Rename file
mv middleware.ts proxy.ts
# 2. Rename function in file
# middleware → proxy
Before (middleware.ts):
export function middleware(request: NextRequest) {
return NextResponse.next()
}
After (proxy.ts):
export function proxy(request: NextRequest) {
return NextResponse.next()
}
Note: middleware.ts still works in Next.js 16 but is deprecated.
3. Parallel Routes Require default.js ⚠️
What Changed: All parallel routes now REQUIRE explicit default.js files.
Before (Next.js 15):
app/
├── @modal/
│ └── login/
│ └── page.tsx
After (Next.js 16):
app/
├── @modal/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED
default.tsx:
export default function ModalDefault() {
return null
}
Fix: Add default.tsx to every @folder in parallel routes.
4. Removed Features ⚠️
Removed:
- AMP support
next lintcommandserverRuntimeConfigandpublicRuntimeConfigexperimental.pprflag- Automatic
scroll-behavior: smooth - Node.js 18 support
Migration:
AMP:
// Before
export const config = { amp: true }
// After
// No direct replacement - use separate AMP pages or frameworks
Linting:
# Before
npm run lint
# After
npx eslint .
# or
npx biome lint .
Runtime Config:
// Before
module.exports = {
serverRuntimeConfig: { secret: 'abc' },
publicRuntimeConfig: { apiUrl: 'https://api' },
}
// After
// Use environment variables
process.env.SECRET
process.env.NEXT_PUBLIC_API_URL
5. Version Requirements ⚠️
Minimum Versions:
- Node.js: 20.9+ (Node 18 removed)
- TypeScript: 5.1+
- React: 19.2+
- Browsers: Chrome 111+, Safari 16.4+, Firefox 109+
Upgrade Node.js:
# Check current version
node --version
# Upgrade (using nvm)
nvm install 20
nvm use 20
nvm alias default 20
# Verify
node --version # Should be 20.9+
6. Image Defaults Changed ⚠️
What Changed: next/image default settings changed.
| Setting | Next.js 15 | Next.js 16 |
|---|---|---|
| TTL | 60s | 4 hours |
| imageSizes | 8 sizes | 5 sizes |
| qualities | 3 qualities | 1 quality (75) |
Impact: Images cache longer, fewer sizes generated.
Revert (if needed):
// next.config.ts
const config = {
images: {
minimumCacheTTL: 60, // Revert to 60 seconds
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Old sizes
},
}
New Features
1. Cache Components ✨
Opt-in caching with "use cache" directive.
Before (Next.js 15 - implicit caching):
// All Server Components cached by default
export async function MyComponent() {
const data = await fetch('/api/data')
return <div>{data.value}</div>
}
After (Next.js 16 - opt-in):
// NOT cached by default
export async function MyComponent() {
const data = await fetch('/api/data')
return <div>{data.value}</div>
}
// Opt-in to caching
'use cache'
export async function CachedComponent() {
const data = await fetch('/api/data')
return <div>{data.value}</div>
}
See: references/cache-components-guide.md
2. Updated Caching APIs ✨
revalidateTag() now requires 2 arguments:
Before:
revalidateTag('posts')
After:
revalidateTag('posts', 'max') // Second argument required
New APIs:
updateTag()- Immediate refresh (read-your-writes)refresh()- Refresh uncached data only
3. React 19.2 Integration ✨
New React features:
- View Transitions
useEffectEvent()(experimental)- React Compiler (stable)
See: references/react-19-integration.md
4. Turbopack Stable ✨
Default bundler: Turbopack is now stable and default.
Metrics:
- 2–5× faster production builds
- Up to 10× faster Fast Refresh
Opt-out (if incompatible):
npm run build -- --webpack
Migration Steps
Step 1: Prerequisites
-
Backup your project:
git commit -am "Pre-migration checkpoint" -
Check Node.js version:
node --version # Should be 20.9+ -
Update dependencies:
npm install next@16 react@19.2 react-dom@19.2
Step 2: Run Automated Codemod
npx @next/codemod@canary upgrade latest
What it fixes:
- ✅ Async params (adds
await) - ✅ Async searchParams
- ✅ Async cookies()
- ✅ Async headers()
- ✅ Updates TypeScript types
What it does NOT fix:
- ❌ middleware.ts → proxy.ts (manual)
- ❌ Parallel routes default.js (manual)
- ❌ Removed features (manual)
Step 3: Manual Fixes
Fix 1: Migrate middleware.ts → proxy.ts
# Rename file
mv middleware.ts proxy.ts
# Update function name
# middleware → proxy
Fix 2: Add default.js to Parallel Routes
# For each @folder, create default.tsx
touch app/@modal/default.tsx
touch app/@feed/default.tsx
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}
Fix 3: Replace Removed Features
AMP: Remove AMP config or migrate to separate AMP implementation.
Linting: Update scripts in package.json:
{
"scripts": {
"lint": "eslint ."
}
}
Runtime Config: Use environment variables.
Step 4: Update Caching
Migrate from implicit to explicit caching:
- Find Server Components with expensive operations
- Add
"use cache"directive - Update
revalidateTag()calls to includecacheLife
Example:
// Before
export async function ExpensiveComponent() {
const data = await fetch('/api/data') // Cached implicitly
return <div>{data.value}</div>
}
// After
'use cache'
export async function ExpensiveComponent() {
const data = await fetch('/api/data') // Cached explicitly
return <div>{data.value}</div>
}
Step 5: Test
# Development
npm run dev
# Production build
npm run build
# Check for errors
npm run type-check
Step 6: Update CI/CD
Update Node.js version in CI config:
.github/workflows/ci.yml:
- uses: actions/setup-node@v4
with:
node-version: '20.9' # Update from 18
Dockerfile:
FROM node:20.9-alpine # Update from node:18
Automated Migration
Codemod (recommended):
npx @next/codemod@canary upgrade latest
Options:
--dry- Preview changes without applying--force- Skip confirmation prompts
What it migrates:
- ✅ Async params
- ✅ Async searchParams
- ✅ Async cookies()
- ✅ Async headers()
- ✅ TypeScript types
Manual steps after codemod:
- Rename middleware.ts → proxy.ts
- Add default.js to parallel routes
- Replace removed features
- Update caching patterns
Manual Migration
If codemod fails or you prefer manual migration:
1. Async Params
Find:
grep -r "params\." app/
grep -r "searchParams\." app/
Replace:
// Before
const slug = params.slug
// After
const { slug } = await params
2. Async Cookies/Headers
Find:
grep -r "cookies()" app/
grep -r "headers()" app/
Replace:
// Before
const cookieStore = cookies()
// After
const cookieStore = await cookies()
3. TypeScript Types
Find: All PageProps types
Replace:
// Before
type PageProps = {
params: { id: string }
searchParams: { q: string }
}
// After
type PageProps = {
params: Promise<{ id: string }>
searchParams: Promise<{ q: string }>
}
Troubleshooting
Error: params is a Promise
Cause: Not awaiting params in Next.js 16.
Fix:
// ❌ Before
const id = params.id
// ✅ After
const { id } = await params
Error: Parallel route missing default.js
Cause: Next.js 16 requires default.js for all parallel routes.
Fix: Create default.tsx:
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}
Error: revalidateTag requires 2 arguments
Cause: revalidateTag() API changed in Next.js 16.
Fix:
// ❌ Before
revalidateTag('posts')
// ✅ After
revalidateTag('posts', 'max')
Error: Turbopack build failure
Cause: Turbopack is now default in Next.js 16.
Fix: Opt-out if incompatible:
npm run build -- --webpack
Error: Node.js version too old
Cause: Next.js 16 requires Node.js 20.9+.
Fix: Upgrade Node.js:
nvm install 20
nvm use 20
nvm alias default 20
Migration Checklist
- Backup project (git commit)
- Check Node.js version (20.9+)
- Update dependencies (
npm install next@16 react@19.2 react-dom@19.2) - Run codemod (
npx @next/codemod@canary upgrade latest) - Rename middleware.ts → proxy.ts
- Add default.js to parallel routes
- Remove AMP config (if used)
- Replace runtime config with env vars
- Update
revalidateTag()calls (addcacheLife) - Add
"use cache"where needed - Test dev server (
npm run dev) - Test production build (
npm run build) - Update CI/CD Node.js version
- Deploy to staging
- Deploy to production
Resources
- Next.js 16 Blog: https://nextjs.org/blog/next-16
- Codemod:
npx @next/codemod@canary upgrade latest - Templates: See
templates/directory - Common Errors: See
references/top-errors.md
Migration Support: jeremy@jezweb.net