portfolio Anshul Bisen
ask my work

Next.js 15, Turbopack stable, and the mass codebase migration nobody is talking about

Next.js 15 shipped with Turbopack stable and async request APIs that break every existing middleware and dynamic page in your application.

Next.js 15 shipped on October 21st and the headline was Turbopack stable for dev. The 76.7% faster startup benchmark made it to every tech newsletter. What did not make the newsletters was the breaking change buried in the migration guide: params, headers, cookies, and searchParams in pages, layouts, and route handlers are now asynchronous. Every dynamic page and every piece of middleware in your application needs to be updated. For our 400-route application, that meant touching 200 files.

The shape of the problem before the fix.

Back then I was still proving I could think like a head of engineering without losing the hands-on instinct that got me there. It also builds on what I learned earlier in “The night our database ran out of connections and what I learned about pooling.” The same full-stack bias shows up later in portfolio and bisen-apps: keep the surface area small, own the sharp edges, and do not create distributed-systems ceremony before the product has earned it.

Where product decisions met implementation reality.

The Turbopack Numbers Are Real

I benchmarked Turbopack against webpack on our actual codebase, not a synthetic demo. The results matched the marketing claims within a reasonable margin.

  • Cold start (first dev server launch): 4.2 seconds with Turbopack versus 11.8 seconds with webpack. A 64% improvement. The marketing claimed 76.7% on their benchmark but our codebase is larger with more dynamic routes.
  • Hot module replacement: 120ms with Turbopack versus 380ms with webpack for a typical component edit. This is the metric that matters most for developer experience. Sub-200ms HMR feels instant.
  • Route compilation (navigating to an unvisited route): 280ms with Turbopack versus 1.1 seconds with webpack. This was the most noticeable improvement in daily development. Clicking a nav link and waiting a full second for the page to compile was a constant friction point.

The cold start improvement alone justified the migration. Starting the dev server 20 times per day (restart after config changes, after installing packages, after pulling from main) at 7 seconds saved per restart adds up to over two minutes of waiting eliminated daily.

The Async API Migration

The performance gains came free with the Next.js 15 upgrade. The breaking change did not. Next.js 15 requires that params, cookies, headers, and searchParams be awaited. In Next.js 14, these were synchronous objects you could destructure immediately. In Next.js 15, they are Promises.

// BEFORE (Next.js 14): synchronous params
export default function PaymentPage({
params,
}: {
params: { id: string };
}) {
const payment = await getPayment(params.id);
return <PaymentDetail payment={payment} />;
}
// AFTER (Next.js 15): async params
export default async function PaymentPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const payment = await getPayment(id);
return <PaymentDetail payment={payment} />;
}

This change affects every dynamic page, every layout that reads params, every middleware that accesses headers or cookies, and every route handler. In our codebase, that was 200 files. Doing this by hand would take days. I wrote a codemod.

The Migration Script

Next.js provides an official codemod via @next/codemod but when I ran it on our codebase it missed several patterns: route handlers with destructured params, middleware that accessed headers in helper functions, and layouts that passed params to child components. I wrote a supplementary script that caught the patterns the official codemod missed.

// Simplified version of the migration script
import { Project } from 'ts-morph';
const project = new Project({ tsConfigFilePath: 'tsconfig.json' });
for (const file of project.getSourceFiles('src/app/**/*.tsx')) {
const defaultExport = file.getDefaultExportSymbol();
if (!defaultExport) continue;
// Find params parameter and wrap type in Promise
const func = defaultExport.getDeclarations()[0];
const paramsParam = func.getParameter('params');
if (!paramsParam) continue;
// Add async keyword if not present
// Wrap params type in Promise<>
// Add await before params destructuring
// ... (actual implementation handles edge cases)
}

The combination of the official codemod and my supplementary script handled 188 of the 200 files automatically. The remaining 12 had unusual patterns that required manual updates: dynamic route handlers that passed params through three levels of function calls, and a middleware that cached headers in a module-level variable.

Patterns That Made It Survivable

Three patterns in our codebase made the migration dramatically easier than it could have been.

  • Centralized param extraction: Most of our pages called a getRouteParams() helper that extracted and validated params. Updating that single helper fixed 60% of our pages in one commit.
  • Typed middleware: Our middleware was written with explicit TypeScript types for the request object. The type errors from the async change immediately highlighted every file that needed updating.
  • Route handler factories: We had a createRouteHandler() wrapper that handled authentication and error handling. Adding async param extraction to the factory fixed all 40 route handlers at once.

The lesson is that abstraction layers pay for themselves during migrations. Every centralized helper that we wrote to reduce code duplication became a single point of change during the Next.js 15 upgrade. The pages that were hardest to migrate were the ones that accessed params directly instead of through a helper.

Was It Worth It

The migration took three days of focused work. The Turbopack performance gains save about two minutes per developer per day. With two developers, that is four minutes per day, or roughly 16 hours over a year. Three days of migration effort for 16 hours of annual time savings is a clear win, and the developer experience improvement is worth more than the raw time calculation suggests. Fast HMR and instant route compilation remove friction from the development loop in a way that compounds across every feature you build.

What changed once the system matured.

Looking back, this is one of those builder-phase decisions that bought me leadership credibility before I had any leadership title equity. I was still proving I could be trusted with the boring, consequential calls. That instinct carried straight into portfolio, pipeline-sdk, and dotfiles.

Upgrade to Next.js 15 for Turbopack. Budget three days for the async API migration. Write a codemod if you have more than 50 dynamic routes. And centralize your param extraction so the next migration is a one-file change.