Next.js 16 Migration Guide: Turbopack as Default Bundler
Our production app took 4 hours to migrate from Next.js 15 to 16. Most of that time went to two issues: async params migration across 23 route handlers and one stubborn Webpack plugin that Turbopack didn't support. Here's the complete migration process, including the gotchas that documentation glosses over.
Pre-Migration Checklist
Before touching any code, verify these requirements:
Node.js version: Next.js 16 requires Node.js 20.9.0 or higher. Check with node -v.
Git status: Ensure your working directory is clean. The automated codemods work best with no uncommitted changes.
Dependency audit: Review your package.json and next.config.js for potential incompatibilities:
# Check for custom Webpack configuration
grep -r "webpack" next.config.js
# List packages that might have Turbopack issues
npm ls | grep -E "(loader|plugin)"Test baseline: Run your test suite and note any existing failures. You'll want to distinguish migration regressions from pre-existing issues.
The Upgrade Command
Start with the automated upgrade:
npx next@latest upgradeThis command:
- Updates
next,react, andreact-domto latest versions - Renames
middleware.tstoproxy.ts - Updates TypeScript configuration
- Runs codemods for common breaking changes
For more control, upgrade manually:
npm install next@latest react@latest react-dom@latest
npm install -D @types/react@latest @types/react-dom@latestBreaking Change: Turbopack Default
Turbopack replaces Webpack as the default bundler. This brings 2-3x faster builds but breaks custom Webpack configurations.
Identifying Webpack Dependencies
Check your next.config.js for the webpack key:
// next.config.js - This will be ignored by Turbopack
module.exports = {
webpack: (config) => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};Temporary Webpack Fallback
If you need more time to migrate, disable Turbopack temporarily:
// next.config.js
module.exports = {
turbo: false, // Falls back to Webpack
};Or use the CLI flag:
next dev --webpack
next build --webpackThis buys time but isn't a long-term solution. Plan to migrate fully.
Common Turbopack Incompatibilities
SVG imports with SVGR: The @svgr/webpack loader doesn't work with Turbopack. Migrate to Next.js's built-in image handling or inline SVGs:
// Before: @svgr/webpack
import Logo from './logo.svg';
<Logo className="h-8 w-8" />
// After: Next.js Image or inline
import Image from 'next/image';
<Image src="/logo.svg" alt="Logo" width={32} height={32} />
// Or: React component
export function Logo({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24">
{/* SVG content */}
</svg>
);
}CSS Modules with custom configuration: Most CSS Modules work, but custom PostCSS plugins may need updates.
Third-party loaders: Check each loader's GitHub for Turbopack compatibility. Many have updated versions.
Breaking Change: Async Request APIs
All request-handling functions must be async. Synchronous access to params, searchParams, cookies, and headers throws runtime errors.
Route Handlers
// Before (Next.js 15)
export function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
return Response.json({ id });
}
// After (Next.js 16)
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
return Response.json({ id });
}Page Components
// Before
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
// After
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}Search Params
// Before
export default function SearchPage({
searchParams,
}: {
searchParams: { q?: string };
}) {
return <Results query={searchParams.q} />;
}
// After
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
return <Results query={q} />;
}Cookies and Headers
// Before
import { cookies, headers } from 'next/headers';
export function GET() {
const token = cookies().get('token');
const userAgent = headers().get('user-agent');
// ...
}
// After
import { cookies, headers } from 'next/headers';
export async function GET() {
const cookieStore = await cookies();
const headersList = await headers();
const token = cookieStore.get('token');
const userAgent = headersList.get('user-agent');
// ...
}Running the Codemod
The async codemod handles most cases:
npx @next/codemod@latest async-request-api .Review the changes—complex cases may need manual adjustment.
Breaking Change: Middleware to Proxy
The middleware file is renamed and the export changes:
File Rename
# The upgrade command does this, but manually:
mv middleware.ts proxy.tsExport Change
// Before: middleware.ts
export function middleware(request: NextRequest) {
// ...
}
// After: proxy.ts
export function proxy(request: NextRequest) {
// ...
}Config Updates
// Before
module.exports = {
skipMiddlewareUrlNormalize: true,
};
// After
module.exports = {
skipProxyUrlNormalize: true,
};Breaking Change: ESLint Configuration
Next.js 16 removes built-in next lint. Use ESLint directly:
# Run the migration codemod
npx @next/codemod@canary next-lint-to-eslint-cli .
# Install ESLint if not present
npm install -D eslint eslint-config-nextUpdate your scripts in package.json:
{
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
}
}Remove the eslint key from next.config.js:
// Before
module.exports = {
eslint: {
ignoreDuringBuilds: true,
},
};
// After - Remove the eslint key entirely
module.exports = {
// eslint configuration now in .eslintrc
};Migrating to Cache Components
If you used PPR (Partial Pre-Rendering), migrate to Cache Components:
// Before
module.exports = {
experimental: {
ppr: true,
},
};
// After
module.exports = {
cacheComponents: true,
};The behavior is similar but Cache Components provide more control:
// Using Cache Components
async function ProductDetails({ id }: { id: string }) {
'use cache';
const product = await db.products.find(id);
return <ProductCard product={product} />;
}
async function InventoryStatus({ id }: { id: string }) {
// No cache directive - always fresh
const inventory = await inventory.check(id);
return <StockDisplay inventory={inventory} />;
}Migration Timeline Estimates
Based on real migrations:
| App Type | Estimated Time | Main Challenges | |----------|----------------|-----------------| | Marketing site | 30 minutes | SVG imports, minimal async changes | | Blog/content site | 1-2 hours | Markdown processing, async params | | E-commerce | 4-6 hours | Many route handlers, custom Webpack | | SaaS dashboard | 6-8 hours | Complex auth, real-time features |
Step-by-Step Migration Process
1. Create a migration branch
git checkout -b chore/nextjs-16-upgrade2. Run the upgrade
npx next@latest upgrade3. Fix TypeScript errors
Run the TypeScript compiler to find issues:
npx tsc --noEmitThe most common errors are async-related. Fix them systematically.
4. Run the build
npm run buildAddress any build failures. Common issues:
- Turbopack incompatibilities
- Missing async/await
- Removed APIs
5. Test thoroughly
npm run test
npm run dev
# Manual testing of critical paths6. Deploy to staging
Test in a production-like environment before promoting to production.
Troubleshooting Common Issues
"Cannot read properties of undefined (reading 'slug')"
You're accessing params synchronously. Add await:
// Wrong
const { slug } = params;
// Right
const { slug } = await params;"Module not found" for SVG imports
Migrate away from @svgr/webpack. Use inline SVGs or Next.js Image.
Build succeeds but runtime fails
Some issues only appear at runtime. Test all dynamic routes:
# Generate a list of all routes
find app -name "page.tsx" | while read f; do
echo "Testing: ${f%page.tsx}"
doneHydration errors after migration
Check for components that render differently on server and client. The async changes can surface timing issues that were hidden before.
Rollback Plan
If critical issues arise:
# Revert to previous versions
npm install next@15.5.0 react@18.3.0 react-dom@18.3.0
# Restore middleware
mv proxy.ts middleware.tsKeep the migration branch until production is stable.
Post-Migration Optimization
After migration stabilizes:
- Remove Webpack fallback if you used
turbo: false - Enable Cache Components for performance gains
- Review prefetching behavior with the new incremental system
- Update CI/CD to remove
next lintif previously used
The migration to Next.js 16 requires attention to detail, especially around async APIs and Turbopack compatibility. But the performance improvements—faster builds, smarter prefetching, and improved caching—make the investment worthwhile. Plan for dedicated migration time, test thoroughly, and you'll have a smoother experience.