Skip to main content
Tilbake til blogg
DevelopmentMarch 28, 202612 min

Migrating to Next.js 16: A Real-World Guide

Lessons from migrating three production apps to Next.js 16 with Turbopack, React 19, and the new app router patterns.

DB

David Bakke

Founder, Bakke & Co

PostShare
ForsidebildeDevelopment

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

  1. EventRipple — Event management SaaS (React 18 + Vite → Next.js 16)
  2. Mission Control — Internal dashboard (Next.js 14 → 16)
  3. 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.

Next.jsReactWeb Development