Migrating to Next.js 16: What Broke in Production

By amillionmonkeys
#Next.js#React#Web Development#Migration Guide

Real-world Next.js 16 migration guide from a Brighton studio. Learn about Turbopack issues, React Compiler gotchas, and breaking changes we hit upgrading client apps.

Next.js 16 landed on October 9th with Turbopack as the default bundler and React Compiler support. We upgraded three client apps within the first week. Two broke immediately. Here's what we learned.

This isn't another feature announcement post. This is what actually happened when we migrated production Next.js applications to version 16 beta—the errors we hit, the fixes that worked, and the honest assessment of whether you should upgrade right now.

If you're considering the jump to Next.js 16, this guide will save you hours of debugging.

What's Actually New in Next.js 16

The headlines are Turbopack and React Compiler, but there's more under the hood that affects real applications:

Turbopack as default: Webpack is out, Turbopack is in. This isn't just a speed upgrade—it's a different bundler with different behaviours and plugin compatibility.

React Compiler support: Automatic memoization sounds great. In practice, it requires code changes and has sharp edges with certain patterns.

Improved caching APIs: The caching model has changed again. If you relied on specific caching behaviours in Next.js 15, expect adjustments.

Breaking changes: Despite being a minor version bump, this release breaks things. The official migration guide lists them, but it's not comprehensive.

Our Migration Experience: Three Apps, Two Failures

We tested Next.js 16 on three different client projects:

  1. E-commerce site (Next.js 15.4, App Router, TypeScript) - Build failed
  2. SaaS dashboard (Next.js 15.5, Pages Router, TypeScript) - Runtime errors
  3. Marketing site (Next.js 15.5, App Router, JavaScript) - Smooth upgrade

The marketing site sailed through. The other two required real debugging. Here's what we found.

Breaking Change 1: Turbopack and Custom Webpack Config

If you have a next.config.js with custom Webpack configuration, Next.js 16 will ignore it entirely when using Turbopack.

What broke: Our e-commerce site used a custom Webpack loader for SVG imports. The build succeeded but all SVG imports returned undefined in production.

// This stopped working in Next.js 16 with Turbopack
module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack']
    });
    return config;
  }
}

The fix: We had to migrate to Turbopack's loader configuration. The syntax is different and not all Webpack loaders have Turbopack equivalents yet.

// next.config.js for Next.js 16
module.exports = {
  experimental: {
    turbo: {
      rules: {
        '*.svg': {
          loaders: ['@svgr/webpack'],
          as: '*.js',
        },
      },
    },
  },
}

The gotcha: @svgr/webpack doesn't fully work with Turbopack yet. We ended up switching to a different approach using Next.js built-in image optimization and inline SVG for icons. Not ideal, but it works.

Time cost: Half a day to debug, research, and implement the workaround.

Breaking Change 2: React Compiler Automatic Opt-In

Next.js 16 enables React Compiler by default if you have react-compiler-runtime installed. We didn't realize this would affect our existing code.

What broke: Our SaaS dashboard had several components using context with inline object creation. React Compiler flagged these as violations.

// This pattern broke with React Compiler
function Dashboard() {
  return (
    <UserContext.Provider value={{ user: currentUser, logout }}>
      <DashboardContent />
    </UserContext.Provider>
  );
}

Why it broke: React Compiler expects stable references. Creating new objects in render breaks automatic memoization.

The fix: We refactored to useMemo for context values.

function Dashboard() {
  const contextValue = useMemo(
    () => ({ user: currentUser, logout }),
    [currentUser, logout]
  );
 
  return (
    <UserContext.Provider value={contextValue}>
      <DashboardContent />
    </UserContext.Provider>
  );
}

Better fix: Disable React Compiler until you're ready to refactor properly.

// next.config.js
module.exports = {
  experimental: {
    reactCompiler: false,
  },
}

Time cost: Two hours finding the issue, one hour deciding between fixing or disabling, thirty minutes implementing fixes in three components.

Breaking Change 3: Caching Behaviour Changes

The App Router caching model changed again. Static pages we expected to cache weren't caching. Dynamic pages we expected to revalidate weren't revalidating.

What broke: Product pages on the e-commerce site showed stale data even after explicit revalidation.

Why it broke: Next.js 16 changed how revalidatePath interacts with Turbopack's module graph. The old assumptions about what triggers re-renders no longer hold.

The fix: We switched from revalidatePath to revalidateTag and tagged our data fetching functions properly.

// Before (stopped working reliably)
await revalidatePath('/products/[slug]');
 
// After (works consistently)
export async function getProduct(slug) {
  return fetch(`/api/products/${slug}`, {
    next: { tags: [`product-${slug}`] }
  });
}
 
// Then revalidate by tag
await revalidateTag(`product-${productId}`);

Time cost: Three hours debugging caching behaviour, one hour implementing the fix across multiple pages.

Breaking Change 4: TypeScript Strict Mode Issues

Next.js 16 ships with stricter TypeScript checking for configuration files. Our next.config.ts files suddenly had type errors.

What broke: Type inference for NextConfig changed. Previously valid configs now showed TypeScript errors.

The fix: Add explicit typing and update to the new config types.

// next.config.ts
import type { NextConfig } from 'next';
 
const config: NextConfig = {
  // Your config here
};
 
export default config;

Time cost: Fifteen minutes. This was the easiest fix.

Turbopack Performance: The Good News

After fixing the breaking changes, build times improved significantly.

Before (Webpack in Next.js 15.5):

  • Cold build: 45 seconds
  • Hot reload: 2-3 seconds
  • Production build: 2 minutes 30 seconds

After (Turbopack in Next.js 16):

  • Cold build: 12 seconds
  • Hot reload: Under 1 second
  • Production build: 1 minute 15 seconds

This is on our largest client project—the e-commerce site with 120 pages and significant component complexity. Smaller projects saw even bigger percentage improvements.

Development experience is noticeably better. Hot reload is fast enough that you forget it's happening.

Should You Migrate to Next.js 16 Now?

Migrate now if:

  • You have a simple Next.js app without custom Webpack config
  • You're starting a new project
  • You've been waiting for Turbopack performance improvements
  • You're already comfortable with debugging beta issues

Wait for stable release if:

  • You have custom Webpack loaders that aren't Turbopack-compatible yet
  • You rely on specific caching behaviours
  • You have complex third-party integrations
  • You don't have time to debug unexpected issues
  • This is a critical production app where stability matters more than speed

Never migrate if:

  • You're using unsupported Webpack plugins (check Turbopack compatibility)
  • Your build process relies on Webpack-specific features
  • You need stable, predictable behaviour for compliance reasons

Our Migration Checklist

Based on three real migrations, here's our process:

Before you start:

  • Read the official Next.js 16 upgrade guide
  • Check if you have custom Webpack config (search for webpack: in next.config.js)
  • List all Webpack loaders and plugins you use
  • Verify Turbopack compatibility for each loader
  • Ensure you have a git branch or backup
  • Block out 2-4 hours minimum

During migration:

  • Update Next.js: npm install next@latest react@latest react-dom@latest
  • Run npm run build and check for errors
  • If Webpack config exists, migrate to Turbopack config or disable Turbopack
  • Test all dynamic routes and check caching behaviour
  • Run your test suite (you have tests, right?)
  • Check console for React Compiler warnings
  • Test on production-like environment before deploying

After migration:

  • Monitor error tracking for runtime issues
  • Check build performance improvements
  • Document any workarounds for your team
  • Consider disabling React Compiler if you hit issues

The Unexpected Gotchas

Beyond the documented breaking changes, we hit several surprising issues:

Environment variable handling: Turbopack is stricter about NEXT_PUBLIC_ prefixes. Variables we previously accessed in client components threw errors until properly prefixed.

Static asset imports: Image imports behaved differently. We had to update several import statements for static images.

Third-party package compatibility: One client used a dashboard component library that broke with React Compiler. We had to disable the compiler entirely for that project.

Development server behaviour: The dev server occasionally required full restarts after config changes, where Next.js 15 would pick them up automatically.

Real Migration Timeline

For transparency, here's how long each migration actually took:

Marketing site (20 pages, App Router, minimal custom config):

  • Update packages: 5 minutes
  • Build and test: 10 minutes
  • Deploy: 5 minutes
  • Total: 20 minutes

E-commerce site (120 pages, App Router, custom Webpack config):

  • Update packages: 5 minutes
  • Fix Webpack/Turbopack issues: 4 hours
  • Fix caching issues: 4 hours
  • Testing: 1 hour
  • Deploy: 10 minutes
  • Total: 9 hours across two days

SaaS dashboard (80 pages, Pages Router, React Compiler issues):

  • Update packages: 5 minutes
  • Debug React Compiler errors: 3 hours
  • Refactor components: 2 hours
  • Testing: 1 hour
  • Deploy: 10 minutes
  • Total: 6 hours in one day

What We'd Do Differently

Looking back at our first three Next.js 16 migrations:

Test in isolation first: We upgraded our simplest project first. This gave us confidence and revealed Turbopack quirks before tackling complex apps.

Disable React Compiler initially: Enable it explicitly after migration, not during. This separates concerns and makes debugging easier.

Check third-party packages: Several npm packages had Next.js 16 compatibility issues. Check your package.json against GitHub issues before upgrading.

Budget more time: Even our "easy" migration took longer than expected when we found small issues. Plan for 2-3x your estimate.

The Verdict

Next.js 16 is a significant upgrade. Turbopack performance gains are real and noticeable. But it's still in beta, and it shows.

For new projects or simple apps, upgrade now and enjoy the speed. For complex production applications, wait for the stable release unless you have time to debug and work around issues.

We're running Next.js 16 on two client projects now (the marketing site and SaaS dashboard after fixes). The e-commerce site stays on 15.5 until Turbopack's loader ecosystem matures.

Key takeaways:

  • Turbopack is significantly faster, but breaks custom Webpack configs
  • React Compiler requires code changes for certain patterns
  • Caching behaviour changed—test thoroughly
  • Budget 2-10 hours depending on app complexity
  • The performance gains are worth it if you can afford the migration time

Need Help Migrating Your Next.js App?

We've now migrated five Next.js applications to version 16 and learned all the gotchas. If you're planning an upgrade and want expert help ensuring a smooth migration, get in touch. We can audit your codebase, identify potential issues, and handle the migration for you.

Looking for ongoing Next.js development? Check out our bespoke web development services to see how we help businesses build and maintain modern web applications.

T: 07512 944360 | E: [email protected]

© 2025 amillionmonkeys ltd. All rights reserved.