Why Migrate to Next.js 16?
Next.js 16 landed with some serious improvements: native Turbopack support, React 19 features, improved streaming, and better edge runtime support. After running three production apps on Next.js 14-15, we made the jump.
Here's the full migration story — the good, the bad, and the "why didn't anyone warn me about this?"
The Apps We Migrated
- EventRipple — Event management SaaS (React 18 + Vite → Next.js 16)
- Mission Control — Internal dashboard (Next.js 14 → 16)
- davidbakke.no — Personal website (Next.js 14 → 16)
Each had different challenges. Here's what we learned.
Step 1: Turbopack — Not Always Faster
Turbopack is the default bundler in Next.js 16 dev mode. It's significantly faster for most projects. But we hit a showstopper on davidbakke.no: Turbopack panics when processing certain WebGL shader imports.
# The fix: fall back to webpack
next dev --webpack
Lesson: Always have a webpack fallback plan. Turbopack is great but not universal yet.
Step 2: React 19 Server Components
The biggest architectural shift is the default to Server Components. In Next.js 16, every component in the app directory is a Server Component unless you add 'use client'.
This caught us in three places:
- Event handlers — Can't use onClick in Server Components
- State management — useState/useEffect require 'use client'
- Third-party libraries — Many aren't Server Component compatible
// Before: worked fine in pages router
export default function Button({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
// After: needs 'use client' directive
'use client';
export default function Button({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
Step 3: App Router Gotchas
The app router is mature in Next.js 16, but there are still patterns that trip people up:
Dynamic Routes with Async Params
// Next.js 16: params is now a Promise
interface Props {
params: Promise<{ slug: string }>;
}
export default async function Page({ params }: Props) {
const { slug } = await params;
// ...
}
Metadata Generation
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post.title };
}
Step 4: Tailwind CSS 4
We upgraded to Tailwind CSS 4 simultaneously. The new CSS-first configuration is cleaner but requires rethinking your setup:
/* Old: tailwind.config.js */
/* New: @import in CSS */
@import "tailwindcss";
@theme {
--color-accent: #39FF85;
--font-display: "JetBrains Mono", monospace;
}
Results
After migration:
- Dev server startup: 8s → 2s (Turbopack, where it works)
- Build time: 45s → 28s
- Bundle size: -15% (automatic code splitting improvements)
- Lighthouse score: 92 → 97
Should You Migrate?
Yes, if:
- You're starting a new project
- You need React 19 features (Actions, use())
- Your dependencies are Server Component compatible
Wait, if:
- You have heavy WebGL/WASM dependencies
- Your team isn't familiar with Server Components
- You're mid-sprint on critical features
This guide is based on migrations completed in January-February 2026. Next.js evolves fast — always check the official docs for the latest patterns.